This is the abridged developer documentation for Vertex
---
# Vertex
> A statically typed language for Salesforce. Compiles to Apex, runs locally too.
Vertex is very early The language, standard library, and tooling are all still moving. Expect breaking changes between releases, pin to a specific version if you use it on something real, and please [file issues](https://github.com/vertex-run/vertex/issues/new/choose) when things break or feel wrong. Feedback right now is what shapes where Vertex goes next. Here is a taste: ```vertex fn validate_age(age: Int): Result { if age < 0 { Error("age cannot be negative") } else if age > 150 { Error("age is unrealistically large") } else { Ok(age) } } fn label(age: Int): String { if age < 18 { "minor" } else { "adult" } } debug match validate_age(25) { Ok(a) => "${label(a)}, age ${a}", Error(e) => "invalid: ${e}", } // adult, age 25 ``` Sum types, exhaustive pattern matching, string interpolation, and a `Result` the compiler makes you deal with. Paste it into a file and run it with `vertex run`, no org required. ## What you get [Section titled “What you get”](#what-you-get) If it compiles, it runs Static types, exhaustive `match`, `Option` in place of `null`, and `Result` in place of exceptions. The compiler catches a lot of what ships as a production bug in Apex. Features from this decade Sum types, pattern matching, `Option`, `Result`, a pipe operator, type inference, immutability by default. The things Apex developers have wanted for a while. One way to do things No class inheritance, no static/instance split, no implicit null. Fewer ways for a program to surprise you. Local dev loop Write, run, and test without touching a Salesforce org. The JIT runs your code locally in milliseconds. When you’re ready, `vertex build` emits Apex classes you deploy the usual way. Errors that actually help Every diagnostic has a title, notes, a `help:` line, and spans on the relevant places. Readable by a human and structured enough for an LLM pair. ## A few quick comparisons [Section titled “A few quick comparisons”](#a-few-quick-comparisons) No more null pointers. Absence is `Option` and you have to handle both cases to get at the value: ```vertex type User { User(name: String) } fn greet(user: Option): String { user .map(fn(u: User): String { "Hello, ${u.name}!" }) .unwrapOr("Hello, stranger!") } debug greet(Some(User(name: "Alice"))) // Hello, Alice! debug greet(None) // Hello, stranger! ``` No more untyped `throw`. Fallible functions return `Result`, and `?` propagates the error when you want the happy path to read cleanly: ```vertex fn check_positive(n: Int): Result { if n < 0 { Error("negative: ${n}") } else { Ok(n) } } fn process(raw: Int): Result { let n = check_positive(raw)? Ok(if n < 18 { "minor" } else { "adult" }) } debug process(25) // Ok(value: adult) debug process(-1) // Error(error: negative: -1) ``` And no more waiting on a deploy every time you want to try something: ```plaintext $ vertex run hello.vtx Hello, Alice! ``` ## About [Section titled “About”](#about) Vertex is a statically typed language that compiles to Salesforce Apex. It borrows from Gleam (`Result`, `Option`, the pipe), Kotlin (named parameters, records), Rust (`?`, explicit mutability), F# (pipeline thinking), and TypeScript (type inference feel), over a syntactic baseline that should look familiar if you write Apex. It targets the Salesforce platform; it is not a general-purpose language. Existing Apex is meant to stay as Apex. Vertex is for new code and incremental adoption via a strong FFI. Head to [Getting Started](/getting-started/) to install and run your first program, or dig into the [Language Tour](/tour/01-hello-world/). If you prefer reading a real codebase, there is a sample library management app at [vertex-run/vertex-library-management](https://github.com/vertex-run/vertex-library-management).
---
# Why Vertex?
> What Vertex actually gives you over Apex, with examples.
Apex is fine. It does the job, it runs on every Salesforce org, and if you have been writing it for a decade you know where the sharp edges are. The sharp edges are the problem. Vertex is an attempt to fix a specific handful of them while keeping the code deployable as Apex. What follows is the list of things you get from Vertex that you do not get from Apex, each with an example. ## The compiler catches more [Section titled “The compiler catches more”](#the-compiler-catches-more) Vertex is statically typed, `match` is checked for exhaustiveness, there is no `null`, and fallible things return `Result`. Put together, a lot of what usually surfaces as a production bug in Apex just fails to compile. ```vertex type Payment { Card(last4: String), Bank(routing: String, account: String), Cash, } fn describe(p: Payment): String { match p { Card(last4:) => "card ending ${last4}", Bank(routing:, account:) => "bank ${routing}/${account}", Cash => "cash", } } debug describe(Card(last4: "1234")) // card ending 1234 ``` Add a `Crypto` variant next year and every `match` that forgot to handle it stops compiling. You find out at `vertex build` time instead of in production. See [Sum Types](/reference/sum-types/), [Pattern Matching](/reference/pattern-matching/), [Result](/reference/result/), and [Option](/reference/option/). ## Features Apex has been missing [Section titled “Features Apex has been missing”](#features-apex-has-been-missing) Apex has been on the Salesforce platform since 2008. Vertex lets you write what you have probably been wishing the language had for a while: ```vertex let total = [1, 2, 3, 4, 5] .filter(fn(n: Int): Bool { n > 2 }) .map(fn(n: Int): Int { n * n }) .fold(0, fn(acc: Int, n: Int): Int { acc + n }) debug total // 50 ``` That is a list literal, the pipe-friendly method chain, higher-order functions, closures, generics inferred end to end, and immutable bindings. You will not find yourself typing `List` three times to do this. See [Functions](/reference/functions/) and [Collections](/reference/collections/). ## Less to remember [Section titled “Less to remember”](#less-to-remember) Vertex picks one way to do each thing and skips the rest: * No class inheritance. Composition with interfaces. * No static vs instance split. There are no static methods; module-level functions exist, and that is the whole story. * No `null`. `Option` handles absence and the compiler checks you handle it. * No exceptions in user code. `Result` handles failure. * Immutability by default. `mutable` is a keyword you type on the declarations that need to change. * File = module. No header, no `package`, no manifest. Fewer features is fine if what you remove were the features that caused the bugs. Most of this list is “remove something that bit us”. See [Modules & Imports](/reference/imports/) and [Bindings](/reference/bindings/). ## A local dev loop [Section titled “A local dev loop”](#a-local-dev-loop) Apex development lives on the platform. Every iteration wants a deploy; every test wants test classes on the org; the feedback loop runs in minutes. Vertex has a JIT. Write a file, run it, see what happens: ```plaintext $ cat hello.vtx debug "hello, vertex!" $ vertex run hello.vtx hello, vertex! ``` Tests work the same way, with a simulated DML layer so even data-access code runs without an org: ```plaintext $ vertex test src/billing/invoice_test.vtx ✓ invoice totals include tax ✓ zero-line invoice is rejected 2 passed, 0 failed (47ms) ``` When you are ready to deploy, `vertex build` emits Apex class files your existing Salesforce tooling picks up directly. See [The vertex CLI](/tooling/cli/) and [Testing](/reference/testing/). ## Errors that actually help [Section titled “Errors that actually help”](#errors-that-actually-help) Most compilers write messages for the compiler author. Vertex writes them for the reader, who is almost always stuck. Every diagnostic comes with a title in plain English, notes about what the compiler saw, an actionable `help:` line, and spans on both the mistake and the declaration that conflicts with it: ```plaintext error[E203]: cannot assign to immutable binding at hello.vtx:3:3 | 2 | let counter = 0 3 | counter = counter + 1 | ^^^^^^^ 'counter' cannot be reassigned notes: 'counter' was declared with 'let', which makes it immutable. help: Change 'let' to 'mutable let' to allow reassignment: mutable let counter = 0 related: 'counter' first declared here at hello.vtx:2:5 ``` The format is deliberate, and one of the reasons is that an LLM coding assistant can parse it cleanly: the error code identifies the category, the related span tells the model which other line to look at, and the `help:` section gives a literal replacement it can apply. See [Compiler Diagnostics](/reference/diagnostics/). ## What Vertex does not try to do [Section titled “What Vertex does not try to do”](#what-vertex-does-not-try-to-do) * It is not a general-purpose language. It targets the Salesforce platform and emits Apex. * It is not an Apex replacement. Existing Apex stays as Apex; Vertex is for new code and incremental adoption. * It is not a scripting language, even though `vertex run` feels like one. It is statically typed and compiled (to Apex) or JIT-executed locally. ## Where it borrows from [Section titled “Where it borrows from”](#where-it-borrows-from) | Language | What Vertex takes from it | | ---------- | -------------------------------------------------------------------------------- | | Gleam | `Result` / `Option`, pipe-friendly method chaining, immutability, no inheritance | | Kotlin | Named parameters, record-style types, exhaustive `match` | | TypeScript | Type-inference feel, string interpolation | | Rust | `?` error propagation, explicit mutability | | F# | Railway Oriented Programming, pipeline thinking | | Apex | Syntactic baseline, `@Annotation` syntax, interface names | If this sounds like something you want to try, head to [Getting Started](/getting-started/). If you want the full walkthrough, start with the [Language Tour](/tour/01-hello-world/).
---
# Coming from Apex
> A side-by-side cheatsheet for Apex developers learning Vertex.
If you already know Apex, you already know most of what Vertex looks like: braces, named arguments, `@Annotations`, `public`, and so on. This page is a quick translation table for the constructs that differ. For the full walkthrough, start with the [Language Tour](/tour/01-hello-world/). ## Primitive types [Section titled “Primitive types”](#primitive-types) | Apex | Vertex | | --------- | --------- | | `Integer` | `Int` | | `Long` | `Long` | | `Double` | `Double` | | `Decimal` | `Decimal` | | `String` | `String` | | `Boolean` | `Bool` | | `Id` | `Id` | | `void` | `Void` | ## Bindings and constants [Section titled “Bindings and constants”](#bindings-and-constants) | Apex | Vertex | | ------------------------------------------ | ----------------------------------- | | `String name = 'Alice';` | `let name = "Alice"` | | `Integer x = 1; x = 2;` *(mutable)* | `mutable let x = 1; x = 2` | | `public static final String ENV = 'prod';` | `const ENV = "prod"` (module level) | | `// unused` variable warning | prefix with `_`: `let _tmp = ...` | Vertex bindings are immutable by default. Add `mutable` when you really need reassignment, so mutation is always visible in the source. ## Strings [Section titled “Strings”](#strings) | Apex | Vertex | | ------------------------ | ---------------------------------- | | `'Hello, ' + name + '!'` | `"Hello, ${name}!"` | | `String.valueOf(x)` | `"${x}"` *(interpolation coerces)* | | `s.toUpperCase()` | `s.toUpperCase()` | | `s.contains('foo')` | `s.contains("foo")` | Strings are always double-quoted. Single quotes are not string literals. ## Control flow [Section titled “Control flow”](#control-flow) Apex: ```apex String label; if (age < 18) label = 'minor'; else if (age < 65) label = 'adult'; else label = 'senior'; ``` Vertex (blocks are expressions, so `if` returns a value): ```vertex let label = if age < 18 { "minor" } else if age < 65 { "adult" } else { "senior" } ``` There is no ternary operator. An `if` expression is the ternary. ## Loops [Section titled “Loops”](#loops) Apex uses C-style `for`. Vertex uses `for..in`. ```apex for (Integer i : numbers) { System.debug(i); } ``` ```vertex for n in numbers { debug n } ``` There is no `while` in user code; idiomatic Vertex expresses iteration with `for..in`, recursion, or collection combinators (`map`, `filter`, `fold`). ## Collections [Section titled “Collections”](#collections) Apex: ```apex List names = new List{ 'Alice', 'Bob' }; Set ids = new Set{ 1, 2, 3 }; Map ages = new Map{ 'Alice' => 30, 'Bob' => 25 }; ``` Vertex: ```vertex let names: List = ["Alice", "Bob"] let ids: Set = #{1, 2, 3} let ages: Map = {"Alice": 30, "Bob": 25} ``` Vertex collections come with the combinators you expect (`map`, `filter`, `fold`, `any`, `all`), so a lot of explicit for-loops go away. See [Collections](/reference/collections/). ## Absence: no null [Section titled “Absence: no null”](#absence-no-null) Apex uses `null` for absence. Vertex has no `null`: use `Option`. ```apex String name = lookup(id); if (name != null) { System.debug('Found: ' + name); } else { System.debug('Missing'); } ``` ```vertex match lookup(id) { Some(name) => debug "Found: ${name}", None => debug "Missing", } ``` Shorthand: `.unwrapOr(default)`, `.map(...)`, `.flatMap(...)`. See [Option](/reference/option/). ## Failure: no exceptions [Section titled “Failure: no exceptions”](#failure-no-exceptions) Apex uses `throw` / `try` / `catch`. Vertex uses `Result`. ```apex public static Integer safeDivide(Integer a, Integer b) { if (b == 0) { throw new IllegalArgumentException('cannot divide by zero'); } return a / b; } ``` ```vertex fn safe_divide(a: Int, b: Int): Result { if b == 0 { Error("cannot divide by zero") } else { Ok(a / b) } } ``` Propagate with the `?` operator: ```vertex fn halve_then_double(a: Int, b: Int): Result { let q = safe_divide(a, b)? // returns early on Error Ok(q * 2) } ``` See [Result](/reference/result/). ## Functions vs classes and statics [Section titled “Functions vs classes and statics”](#functions-vs-classes-and-statics) Apex lives inside classes. Vertex does not have static methods: a `.vtx` file *is* a module, and its `pub fn` declarations are its API. ```apex // Apex public class MathUtils { public static Integer add(Integer a, Integer b) { return a + b; } } Integer sum = MathUtils.add(1, 2); ``` math\_utils.vtx ```vertex pub fn add(a: Int, b: Int): Int { a + b } ``` ```vertex // in another file import math_utils let sum = math_utils.add(1, 2) ``` See [Functions](/reference/functions/) and [Modules & Imports](/reference/imports/). ## Records and sum types [Section titled “Records and sum types”](#records-and-sum-types) Apex has classes (with fields, getters/setters, constructors) and enums. Vertex has `type`, which covers both. ```apex public class Account { public String name; public Decimal revenue; public Account(String name, Decimal revenue) { this.name = name; this.revenue = revenue; } } ``` ```vertex type Account { Account(name: String, revenue: Decimal) } let a = Account(name: "ACME", revenue: 1000000.0d) debug a.name ``` Multi-variant types (sum types) have no Apex equivalent; the closest thing is an `enum`, but sum types carry data per variant: ```vertex type Payment { Card(last4: String), Bank(routing: String, account: String), Cash, } ``` See [Sum Types](/reference/sum-types/) and [Pattern Matching](/reference/pattern-matching/). ## SObjects and DML [Section titled “SObjects and DML”](#sobjects-and-dml) ```apex Account a = new Account(Name = 'ACME'); insert a; ``` ```vertex @SObject type Account { Account(Name: String) } let acc = Account(Name: "ACME") let result = Database.insert(acc) // Result ``` Apex’s “throws on failure” becomes an explicit `Result` you must match on. `Database.insert`, `update`, `delete`, and `undelete` all return `Result`. See [Salesforce Integration](/reference/salesforce/integration/). ## `@AuraEnabled` [Section titled “@AuraEnabled”](#auraenabled) ```apex @AuraEnabled(cacheable=true) public static String getAccountName(Id id) { ... } ``` ```vertex @AuraEnabled(cacheable: true) fn getAccountName(id: String): Result { ... } ``` The function must return `Result`. `Ok` is returned to the LWC; `Error` is translated into an `AuraHandledException` at the Apex boundary. See [Annotations](/reference/annotations/). ## Tests [Section titled “Tests”](#tests) ```apex @IsTest private class InvoiceTest { @IsTest static void totals_include_tax() { System.assertEquals(110, Invoice.total(100, 10)); } } ``` invoice\_test.vtx ```vertex @Test fn totals_include_tax(): Void { assert Invoice.total(100, 10) == 110 } ``` Test files end in `_test.vtx`. Run them with `vertex test` (see [Testing](/reference/testing/)). ## Sharing [Section titled “Sharing”](#sharing) ```apex public without sharing class AdminJob { ... } ``` ```vertex @WithoutSharing pub fn run(): Void { ... } ``` The sharing annotation goes at the top of the file. Default is `@WithSharing`, which matches Apex security best practices. ## Annotations reference [Section titled “Annotations reference”](#annotations-reference) | Apex | Vertex | | ------------------------------ | ------------------------------- | | `@IsTest` *(method)* | `@Test` | | `@AuraEnabled` | `@AuraEnabled` | | `@AuraEnabled(cacheable=true)` | `@AuraEnabled(cacheable: true)` | | `with sharing` | `@WithSharing` | | `without sharing` | `@WithoutSharing` | | `inherited sharing` | `@InheritedSharing` | ## Calling existing Apex [Section titled “Calling existing Apex”](#calling-existing-apex) You do not have to rewrite anything. Vertex can call Apex through `extern` declarations: ```vertex extern type Messaging.SingleEmailMessage extern new Messaging.SingleEmailMessage(): Messaging.SingleEmailMessage extern method Messaging.SingleEmailMessage.setSubject( self: Messaging.SingleEmailMessage, subject: String, ): Void let msg = Messaging.SingleEmailMessage.new() msg.setSubject("Hello from Vertex") ``` See [Apex FFI](/reference/salesforce/apex-ffi/). ## Next steps [Section titled “Next steps”](#next-steps) * [Getting Started](/getting-started/): install the toolchain and run your first program. * [Language Tour](/tour/01-hello-world/): a guided walk through the language from the top. * [Guides](/guides/): task-oriented how-tos (error handling, testing, pattern matching).
---
# FizzBuzz
> A complete FizzBuzz example in Vertex using recursive functions and if expressions.
A classic FizzBuzz implementation showing recursive functions and `if` as an expression. ```vertex fn fizzbuzz( n: Int, ): String { let fizz = n % 3 == 0 let buzz = n % 5 == 0 if fizz && buzz { "FizzBuzz" } else if fizz { "Fizz" } else if buzz { "Buzz" } else { "${n}" } } fn run( n: Int, limit: Int, ): Void { if n <= limit { debug fizzbuzz(n) run(n + 1, limit) } } run(1, 20) ``` Key concepts demonstrated: * `if` as an expression that produces a value * String interpolation with `${}` * Recursive functions * `Void`-returning functions that use `if` for control flow
---
# Library Management (sample app)
> A full multi-module Vertex Salesforce app you can read, clone, and deploy.
The examples on this site are mostly focused snippets. If you want to see what a real multi-module Vertex codebase looks like, there is a dedicated sample repo: **[github.com/vertex-run/vertex-library-management](https://github.com/vertex-run/vertex-library-management)** The app models a virtual library: catalog browsing, member checkouts, return history, inventory limits. The Vertex source lives in [`src/`](https://github.com/vertex-run/vertex-library-management/tree/main/src), organized into `domain/` (pure business logic + tests), `controllers/` (`@AuraEnabled` entry points), and top-level modules for the `@SObject` types. It is a good thing to read after the [Language Tour](/tour/01-hello-world/) when you want to see how the pieces fit together at scale: domain types, DML through `Result`, tests that run locally without an org, and LWC-facing controllers. ## Running it yourself [Section titled “Running it yourself”](#running-it-yourself) Clone the repo and follow the README. The short version: ```plaintext $ git clone https://github.com/vertex-run/vertex-library-management.git $ cd vertex-library-management $ sf org create -f config/project-scratch-def.json -a vertex-lib $ vertex build $ sf project deploy start --target-org vertex-lib ``` For the day-to-day development loop (running tests locally, deploying when you are ready), see the [Building for Salesforce](/guides/salesforce-projects/) guide.
---
# Result and Closures
> A complete example combining Result error handling, the ? operator, and first-class functions.
A complete example combining `Result` error handling, the `?` propagation operator, and first-class functions (closures and `make_adder`). ```vertex fn validate_age( age: Int, ): Result { if age < 0 { Error("age cannot be negative") } else if age > 150 { Error("age is unrealistically large") } else { Ok(age) } } fn label_age( age: Int, ): String { if age < 18 { "minor" } else { "adult" } } fn process_age( raw: Int, ): Result { let age = validate_age(raw)? Ok(label_age(age) + " (age " + age + ")") } fn make_adder( base: Int, ): fn(Int): Int { fn(x: Int): Int { base + x } } debug match process_age(25) { Ok(s) => s, Error(e) => "invalid: " + e, } // adult (age 25) let add10 = make_adder(10) debug add10(5) // 15 debug 5 |> add10 // 15 ``` Key concepts demonstrated: * `Result` for error handling without exceptions * `?` operator for early error propagation in a function returning `Result` * Closures capturing their enclosing scope (`base` in `make_adder`) * `|>` pipe operator as an alternative to function call syntax * `match` on a `Result` to handle both cases
---
# Getting Started
> Install Vertex, run your first program, and set up your editor.
Welcome. This section gets you from zero to a running Vertex program in a few minutes. It covers installing the `vertex` binary, running your first file, scaffolding a Salesforce project, and wiring up your editor. If you just want to read code, head to the [Language Tour](/tour/01-hello-world/) instead. For a full sample application you can clone and deploy, see the [Library Management sample](/examples/library-management/). ## In this section [Section titled “In this section”](#in-this-section) Install Vertex Download the `vertex` binary for macOS, Linux, or Windows, and put it on your `PATH`. [Read more](/getting-started/install/) Your first program Write a `.vtx` file, run it with `vertex run`, and see output in milliseconds. No Salesforce org required. [Read more](/getting-started/first-program/) Your first Salesforce project Scaffold a multi-file project, build it to Apex classes, and deploy to your scratch org. [Read more](/getting-started/first-salesforce-project/) Editor setup Syntax highlighting, diagnostics, and completions via the Zed extension and the built-in LSP. [Read more](/getting-started/editor-setup/)
---
# Editor setup
> Syntax highlighting, diagnostics, and completions via the built-in LSP.
Vertex ships with a language server (LSP). Any editor that speaks LSP can use it for real-time diagnostics, completions, hover, and go-to-definition. The fastest path today is the Zed extension, which wires everything up for you. ## Zed (recommended) [Section titled “Zed (recommended)”](#zed-recommended) Install the **Vertex** extension from Zed’s extensions pane. It provides: * Syntax highlighting via the Vertex tree-sitter grammar. * Real-time diagnostics as you type. * Completions for keywords, built-in types, stdlib methods, and identifiers in scope. * Hover and go-to-definition for declarations. The extension automatically downloads the correct `vertex` binary for your platform. If you already have `vertex` on your `PATH`, it uses that. Source: [vertex-run/zed-vertex](https://github.com/vertex-run/zed-vertex). ## Other editors [Section titled “Other editors”](#other-editors) Any editor that can run an LSP server can drive the Vertex LSP. The command to launch it is: ```plaintext vertex lsp ``` This reads and writes LSP protocol on stdin/stdout. Exact wiring depends on your editor’s LSP client; the things you will need to configure: * **Language ID**: `vertex` * **File pattern**: `*.vtx` * **Server command**: `vertex lsp` * **Initialization options**: none required For syntax highlighting outside Zed, the tree-sitter grammar lives at [vertex-run/tree-sitter-vertex](https://github.com/vertex-run/tree-sitter-vertex). ## Formatter [Section titled “Formatter”](#formatter) A formatter is not yet available. The language uses braces `{}`, no semicolons, and a 4-space indent as its baseline style; following the examples in the [tour](/tour/01-hello-world/) and [reference](/reference/) will keep you close to idiomatic. ## Next [Section titled “Next”](#next) Now that you are set up, start the [Language Tour](/tour/01-hello-world/).
---
# Your first Vertex program
> Write a .vtx file, run it locally, and see output in milliseconds.
This page assumes you have already [installed Vertex](/getting-started/install/) and `vertex --version` prints a version. ## Hello, Vertex [Section titled “Hello, Vertex”](#hello-vertex) Create a file called `hello.vtx`: ```vertex debug "hello, vertex!" ``` Run it: ```plaintext $ vertex run hello.vtx hello, vertex! ``` That is it. No `main` function, no class wrapper, no `sfdx-project.json`, no deploy. Vertex executes top-level statements in order, top to bottom. ## `debug` is the output function [Section titled “debug is the output function”](#debug-is-the-output-function) `debug` is the only way to produce observable output. It is an **expression**: it evaluates its argument, prints it, and returns the value so you can inspect mid-pipeline. ```vertex debug "hello" debug 42 debug true let sum = debug 1 + 2 // prints 3, then binds sum = 3 ``` ## Bindings [Section titled “Bindings”](#bindings) ```vertex let name = "Alice" let age = 30 debug "Hello, ${name}! You are ${age}." ``` All bindings are immutable by default. If you need to reassign: ```vertex mutable let counter = 0 counter = counter + 1 counter = counter + 1 debug counter // 2 ``` The visible `mutable` keyword is deliberate. Reassignment is possible but it stands out. See [Bindings](/reference/bindings/). ## Doing something useful [Section titled “Doing something useful”](#doing-something-useful) Here is a complete program that pitches several of the ideas the tour will cover in more depth: ```vertex fn label(age: Int): String { if age < 18 { "minor" } else { "adult" } } fn describe(name: String, age: Int): String { "${name} is ${label(age)} (age ${age})" } debug describe("Alice", 30) debug describe("Bob", 15) ``` Run it: ```plaintext $ vertex run greet.vtx Alice is adult (age 30) Bob is minor (age 15) ``` ## What just happened [Section titled “What just happened”](#what-just-happened) * **No deploy.** Vertex ran your code locally via its built-in JIT. * **Compile-time guarantees.** The types of `name`, `age`, and the return of `label` were all checked before anything ran. * **Millisecond feedback.** No org, no build step, no waiting. ## Next [Section titled “Next”](#next) * [Your first Salesforce project](/getting-started/first-salesforce-project/): move from single files to a proper multi-file project that builds to Apex and deploys. * [Language Tour](/tour/01-hello-world/): a guided walk through every language feature.
---
# Your first Salesforce project
> Scaffold a Vertex project, build it to Apex, and deploy to a scratch org.
This page walks you from a single-file Vertex script to a real project that builds to Apex class files and deploys to a Salesforce org. It assumes you have: * Vertex [installed](/getting-started/install/). * The [Salesforce CLI (`sf`)](https://developer.salesforce.com/tools/salesforcecli) installed and authenticated to a scratch org or sandbox. ## Project layout [Section titled “Project layout”](#project-layout) A Vertex project is any directory containing an `sfdx-project.json` file. The file tells the Salesforce tooling where your Apex source lives; Vertex uses the same file to locate your Vertex source. The conventional layout: ```plaintext my-project/ ├── sfdx-project.json ├── src/ # .vtx source goes here │ └── hello.vtx └── force-app/ └── main/ └── default/ └── classes/ # vertex build writes .cls files here ``` A minimal `sfdx-project.json`: ```json { "packageDirectories": [ { "path": "force-app", "default": true } ], "sourceApiVersion": "66.0" } ``` ## Write an entry point [Section titled “Write an entry point”](#write-an-entry-point) Vertex programs that run as real Apex classes expose a `pub fn run` entry point. Create `src/hello.vtx`: ```vertex pub fn run(): Void { debug "Hello from Vertex on Salesforce!" } ``` This compiles to a `public static void vtx_run()` method on a generated Apex class. ## Build [Section titled “Build”](#build) From the project root: ```plaintext $ vertex build Success! Built 1 class(es) to force-app/main/default/classes/ ``` Look inside `force-app/main/default/classes/` and you will see a `.cls` file and a `.cls-meta.xml` file ready for deployment. ## Deploy [Section titled “Deploy”](#deploy) ```plaintext $ sf project deploy start --target-org ``` Use the alias you set up with `sf org login`. Do not pass `--source-dir` or `--metadata`: the `sf` CLI tracks local changes internally. ## Run it [Section titled “Run it”](#run-it) Create `run-hello.apex`: ```apex Hello.vtx_run(); ``` Then: ```plaintext $ sf apex run --target-org --file run-hello.apex ``` You should see your debug line in the Apex log. ## The full development loop [Section titled “The full development loop”](#the-full-development-loop) For day-to-day iteration, you rarely need the org. The loop is: 1. Edit a `.vtx` file. 2. `vertex run ` to execute it locally (millisecond feedback). 3. `vertex test` to run your `*_test.vtx` files. 4. `vertex build` and `sf project deploy start` when you are ready to ship. See [The vertex CLI](/tooling/cli/) for the full command reference and [Testing](/reference/testing/) for the test runner. ## Next [Section titled “Next”](#next) * [Editor setup](/getting-started/editor-setup/): get syntax highlighting, diagnostics, and completions in your editor. * [Guides: Building for Salesforce](/guides/salesforce-projects/): a deeper look at SObjects, DML, and `@AuraEnabled`.
---
# Install Vertex
> Download the vertex binary and put it on your PATH.
Vertex ships as a single binary per platform. Grab the one for your machine from the [latest release](https://github.com/vertex-run/vertex/releases/latest) and put it on your `PATH`. Vertex is very early Before you install: Vertex is in active early development. The language, stdlib, and tooling are still moving and **you should expect breaking changes between releases**. Pin to a specific release tag if you care, and please [file issues](https://github.com/vertex-run/vertex/issues/new/choose) when something breaks, confuses you, or just feels wrong. Early feedback is how this gets less rough. ## Download and install [Section titled “Download and install”](#download-and-install) * macOS (Apple Silicon) ```bash curl -Lo vertex https://github.com/vertex-run/vertex/releases/latest/download/vertex-macos-arm64 chmod +x vertex sudo mv vertex /usr/local/bin/ ``` * macOS (Intel) ```bash curl -Lo vertex https://github.com/vertex-run/vertex/releases/latest/download/vertex-macos-x86_64 chmod +x vertex sudo mv vertex /usr/local/bin/ ``` * Linux (x86\_64) ```bash curl -Lo vertex https://github.com/vertex-run/vertex/releases/latest/download/vertex-linux-x86_64 chmod +x vertex sudo mv vertex /usr/local/bin/ ``` * Windows (x86\_64) Download `vertex-windows-x86_64.exe` from the [releases page](https://github.com/vertex-run/vertex/releases/latest), rename it to `vertex.exe`, and place it in a folder on your `PATH`. ## Verify [Section titled “Verify”](#verify) ```plaintext vertex --version ``` You should see a version string. If you see `command not found`, the binary is not on your `PATH`; double-check the install location. ## What you get [Section titled “What you get”](#what-you-get) The `vertex` binary is everything you need to: * **Run** `.vtx` files locally: `vertex run hello.vtx` * **Test** your code: `vertex test` * **Build** Apex classes for deployment: `vertex build` * **Serve** the language server for your editor: `vertex lsp` See [The vertex CLI](/tooling/cli/) for the full command reference. ## Optional: Salesforce tooling [Section titled “Optional: Salesforce tooling”](#optional-salesforce-tooling) To deploy the Apex classes that `vertex build` emits, you will also want the [Salesforce CLI (`sf`)](https://developer.salesforce.com/tools/salesforcecli). It is not required for writing or running Vertex locally; only for pushing the generated code to an org. ## Next [Section titled “Next”](#next) Run your first program: [Your first Vertex program](/getting-started/first-program/).
---
# Guides
> Task-oriented how-to guides for common Vertex workflows.
The [Language Tour](/tour/01-hello-world/) and [Reference](/reference/) answer *what is in Vertex?*. This section answers *how do I do X?*. Each guide is self-contained and assumes you are comfortable with everything up to tour Chapter 8 (Option and Result). ## In this section [Section titled “In this section”](#in-this-section) Error handling with Result How to design fallible APIs, when to use `?`, and how to build up error types that compose. [Read more](/guides/error-handling/) Pattern matching in practice Real patterns beyond the basics: guards, nested destructuring, list patterns, and when to reach for `let assert`. [Read more](/guides/pattern-matching/) Testing your code Writing `@Test` functions, structuring test files, and using the simulated DML layer to test Salesforce code without an org. [Read more](/guides/testing/) Interop with existing Apex Calling Apex classes, wrapping the parts of `System.*` you need, and handling values of type `Apex.Object`. [Read more](/guides/apex-interop/) Building for Salesforce Putting it all together: `@SObject`, DML, `@AuraEnabled`, and sharing modifiers in a single project. [Read more](/guides/salesforce-projects/)
---
# Interop with existing Apex
> Calling Apex classes from Vertex, wrapping System APIs, and handling Apex.Object.
Vertex is designed for incremental adoption. You should be able to drop a Vertex file into an existing Apex codebase without rewriting anything. This guide walks through the three tools that make that possible: `extern` declarations, `@SObject` types, and the `Apex.Object` escape hatch. For the full reference, see [Apex FFI](/reference/salesforce/apex-ffi/) and [Apex.Object](/reference/salesforce/apex-object/). ## Calling an Apex class [Section titled “Calling an Apex class”](#calling-an-apex-class) You declare the Apex surface you want to use with `extern`. A minimal example: send a single email via `Messaging.SingleEmailMessage`. ```vertex extern type Messaging.SingleEmailMessage extern new Messaging.SingleEmailMessage(): Messaging.SingleEmailMessage extern method Messaging.SingleEmailMessage.setSubject( self: Messaging.SingleEmailMessage, subject: String, ): Void extern method Messaging.SingleEmailMessage.setPlainTextBody( self: Messaging.SingleEmailMessage, body: String, ): Void ``` Then use it like any other type: ```vertex let msg = Messaging.SingleEmailMessage.new() msg.setSubject("Hello") msg.setPlainTextBody("from Vertex") ``` ## Only declare what you use [Section titled “Only declare what you use”](#only-declare-what-you-use) The temptation is to wrap the entire Apex class at once. Don’t. You only need declarations for the methods your Vertex code actually calls. The generated Apex references `Messaging.SingleEmailMessage` directly, so unused methods on the Apex side are still available to your Apex callers. This keeps the Vertex codebase tidy and the surface area small. ## Group `extern` declarations per Apex class [Section titled “Group extern declarations per Apex class”](#group-extern-declarations-per-apex-class) A common pattern is one Vertex module per wrapped Apex class: ```plaintext src/ ├── apex/ │ ├── messaging.vtx # extern wrappers for Messaging.* │ ├── http.vtx # extern wrappers for Http / HttpRequest / HttpResponse │ └── schema.vtx # extern wrappers for Schema.describe* └── billing/ └── invoice.vtx # business logic imports from apex/* ``` Your Vertex business logic stays decoupled from the Apex types; the FFI details live in the wrapper modules. ## Static methods on Apex classes [Section titled “Static methods on Apex classes”](#static-methods-on-apex-classes) Some Apex APIs are static (`Database.query`, `Http.send`, etc.). Use `extern fn` in a module whose name matches the Apex type: apex/json.vtx ```vertex extern fn JSON.serialize(o: Apex.Object): String extern fn JSON.deserializeUntyped(s: String): Apex.Object ``` ## Dealing with `Apex.Object` [Section titled “Dealing with Apex.Object”](#dealing-with-apexobject) Some Apex APIs take or return the universal `Object` type (the root of Apex’s type hierarchy). `JSON.deserializeUntyped` is the canonical example: you hand it a JSON string and it gives back an untyped `Map`, `List`, or scalar. Vertex has a dedicated type `Apex.Object` for this. You convert from a typed Vertex value into `Apex.Object` with `from`, and back out with `as`: ```vertex let raw = JSON.serialize(Apex.Object.from(inv)) let parsed = JSON.deserializeUntyped(raw) let recovered: Result = parsed.as() ``` See [Apex.Object](/reference/salesforce/apex-object/) for the full conversion semantics, including how it handles nulls and type mismatches. ## What does not work in JIT mode [Section titled “What does not work in JIT mode”](#what-does-not-work-in-jit-mode) Any call that goes through an `extern` declaration is **not executed** when you run your code with `vertex run` (JIT). The JIT has no Salesforce runtime to call. In practice this means: * Business logic that only uses Vertex features: testable locally. * Code that calls Apex (DML aside): must be tested on the org. DML calls are a special case: `Database.insert`, `update`, `delete`, and `undelete` are simulated in JIT mode so that most business logic remains testable without an org. ## Two-way interop [Section titled “Two-way interop”](#two-way-interop) Vertex-generated Apex classes can be called from existing Apex code. `pub fn` declarations become `public static` methods with a `vtx_` prefix on the generated class. greet.vtx ```vertex pub fn say_hello(name: String): String { "Hello, ${name}!" } ``` From Apex: ```apex String greeting = Greet.vtx_say_hello('Alice'); ``` This is how a team adopts Vertex incrementally: new code is Vertex; existing Apex callers reach in through the generated API. ## Checklist [Section titled “Checklist”](#checklist) * Declare only the Apex methods you actually call. * Keep `extern` declarations in dedicated wrapper modules. * Use `Apex.Object` when (and only when) the Apex API itself takes or returns `Object`. * Remember that `extern` calls do not run in JIT mode; plan your testing strategy accordingly.
---
# Error handling with Result
> Designing fallible APIs in Vertex, using the ? operator, and building error types that compose.
Vertex has no exceptions in user code. Every fallible operation returns `Result`. This guide is about the design patterns that turn that raw primitive into APIs that are pleasant to use. If you have not met `Result` yet, start with [tour Chapter 8](/tour/08-option-and-result/) and [the Result reference](/reference/result/). ## The core idea [Section titled “The core idea”](#the-core-idea) The compiler’s rule is simple: if your function might fail, it returns `Result`. The type signature is the API contract. Every caller has to handle both the success and the failure case, usually with `match` or the `?` operator. ```vertex fn validate_age(age: Int): Result { if age < 0 { Error("age cannot be negative") } else if age > 150 { Error("age is unrealistically large") } else { Ok(age) } } ``` ## Use `?` when you are forwarding the error [Section titled “Use ? when you are forwarding the error”](#use--when-you-are-forwarding-the-error) The `?` operator is how you say *“propagate this error unchanged if it occurred; otherwise give me the success value.”* It is how you keep the happy path readable. ```vertex fn greet(age: Int): Result { let a = validate_age(age)? Ok(if a < 18 { "hi, kid" } else { "hello" }) } ``` If `validate_age` returns `Error(e)`, `greet` returns `Error(e)` at that point and the rest of the function body does not run. If it returns `Ok(a)`, `a` is bound to the inner value and execution continues. ## Designing error types [Section titled “Designing error types”](#designing-error-types) For small programs, `Result` is fine. For anything that survives a code review, introduce a real error type. A good error type is a sum type with one variant per *kind* of failure: ```vertex type InvoiceError { NegativeAmount(line: Int), ZeroLineItems, InvalidCustomerId(id: String), } fn total_invoice(inv: Invoice): Result { ... } ``` Pattern matching on the call site becomes type-safe and exhaustive: ```vertex match total_invoice(inv) { Ok(total) => debug "${total}", Error(NegativeAmount(line:)) => debug "line ${line} was negative", Error(ZeroLineItems) => debug "no items on invoice", Error(InvalidCustomerId(id:)) => debug "bad customer: ${id}", } ``` ## Crossing error-type boundaries [Section titled “Crossing error-type boundaries”](#crossing-error-type-boundaries) When you call a helper that returns `Result<_, HelperError>` from a function that returns `Result<_, CallerError>`, you need `mapError`: ```vertex fn pricing_job(age: Int): Result { let a = validate_age(age).mapError(fn(msg: String): JobError { InvalidInput(msg) })? // rest of the computation uses `a` Ok(0.0d) } ``` The pattern is: *`helper_call().mapError(wrap_into_my_error_type)?`*. `mapError` converts the error variant only; the success value flows through unchanged. ## Do not reach for exceptions through FFI [Section titled “Do not reach for exceptions through FFI”](#do-not-reach-for-exceptions-through-ffi) Calling Apex can throw. If you are bridging an Apex library, catch the exception at the boundary and convert it to a `Result`: ```vertex fn safe_call(): Result { // inside a well-defined extern wrapper match Database.insert(acc) { Ok(id) => Ok(acc.withId(id)), Error(dmlErr) => Error(dmlErr), } } ``` Do not surface exceptions up through your Vertex code. The point of `Result` is that the caller knows, from the signature, that the operation can fail. Thrown-across-FFI exceptions defeat that. ## `let assert` is a last resort [Section titled “let assert is a last resort”](#let-assert-is-a-last-resort) `let assert` is Vertex’s way of saying *“I know this cannot fail; if I’m wrong, crash.”* It is useful in tests and one-off scripts; it is rarely correct in production code. ```vertex // OK in a test: fixture setup you know is valid let assert Ok(acc) = Database.insert(Account(Name: "Test")) // Not OK in production: you have just traded a Result for a crash ``` When you find yourself wanting `let assert` in real code, consider returning `Result` from the enclosing function instead. ## Checklist [Section titled “Checklist”](#checklist) When reviewing code that uses `Result`: * Does every fallible function say so in its return type? * Are error variants specific enough that a caller can decide what to do, not just log and give up? * Is the `?` operator used where the intent is “forward the error unchanged”? * Is `mapError` used when crossing error-type boundaries? * Is `let assert` reserved for tests and known-valid invariants? If all five answers are yes, the code is in good shape.
---
# Pattern matching in practice
> Real patterns beyond the basics, including guards, nested destructuring, and list patterns.
`match` is Vertex’s main decision-making construct. Once you are comfortable with the basics (Chapter 6 of the tour), you will start reaching for it instead of `if` chains in almost every case. This guide collects the patterns that are not obvious from the reference. ## Matching is the default, `if` is the exception [Section titled “Matching is the default, if is the exception”](#matching-is-the-default-if-is-the-exception) A rough heuristic: if the decision involves *values*, use `if`; if it involves *shapes*, use `match`. ```vertex // Values: a simple inequality if total > 1000 { apply_discount() } else { full_price() } // Shapes: a sum-type variant match payment { Card(last4:) if fraud_check_failed(last4) => Error(Fraud), Card(..) => Ok(Authorized), Bank(..) => Ok(PendingClearance), Cash => Ok(Authorized), } ``` An `if` chain that dispatches on the variant of a sum type is almost always a `match` waiting to happen. ## Guards let you combine values and shapes [Section titled “Guards let you combine values and shapes”](#guards-let-you-combine-values-and-shapes) Guards are the `if` inside an arm. They let one arm match “a Card variant whose `last4` looks suspicious” without carving it out as a separate type. ```vertex match transaction { Sale(amount:) if amount > limit => Error(OverLimit), Sale(amount:) => Ok(amount), Refund(amount:) => Ok(-amount), } ``` Order matters: the first arm that matches wins. Put the guarded arm *before* the unguarded arm for the same variant. ## Destructure records at the call site [Section titled “Destructure records at the call site”](#destructure-records-at-the-call-site) Single-variant record types destructure cleanly: ```vertex type Invoice { Invoice(id: Id, customer: Id, total: Decimal, tax: Decimal) } fn describe(inv: Invoice): String { let Invoice(customer:, total:, tax:, ..) = inv "customer ${customer} owes ${total} plus ${tax} tax" } ``` The `..` silently ignores the unmentioned fields (here, `id`). If you want different names, use the long form `field: binding`: ```vertex match inv { Invoice(total: t, tax:, ..) => debug "${t + tax}", } ``` ## Nested patterns [Section titled “Nested patterns”](#nested-patterns) Patterns nest. Match a `Result` of a sum type variant in one step: ```vertex match charge_card(amount) { Ok(Authorized) => receipt(), Ok(PendingClearance) => "wait for it", Error(CardDecline(Insufficient)) => "try a different card", Error(CardDecline(Expired)) => "card expired", Error(NetworkFailure) => "we'll retry in a bit", } ``` Each layer is a separate pattern. This tends to replace two or three levels of nested `match` in other languages. ## List patterns [Section titled “List patterns”](#list-patterns) Lists match by position, with `..` collecting the tail. ```vertex match events { [] => "nothing happened", [e] => "one event: ${e.name}", [first, ...rest] => "first: ${first.name}, then ${rest.size()} more", } ``` ## OR patterns for shared arms [Section titled “OR patterns for shared arms”](#or-patterns-for-shared-arms) When two variants take the same action, group them with `|`: ```vertex match state { Pending | InReview => debug "waiting", Approved => debug "green light", Rejected => debug "blocked", } ``` OR patterns cannot bind different shapes (you cannot `Card(x) | Bank(x)`) because the bound names would have different meanings. ## When exhaustiveness bites [Section titled “When exhaustiveness bites”](#when-exhaustiveness-bites) The compiler checks `match` arms for exhaustiveness. If you add a new variant to a sum type, every `match` that forgot to handle it fails to compile. That is the point. When you genuinely do not care about the other variants, use a wildcard: ```vertex match status { Approved => ship(), _ => hold(), } ``` The trap: a careless wildcard swallows the variant you added last month. Use `_` only when “everything else” really is one action. ## `let assert` for invariants [Section titled “let assert for invariants”](#let-assert-for-invariants) `let assert` checks at runtime that a pattern matches and binds the inner values, crashing if it doesn’t. It is the right tool for assertions that are *not* user input: ```vertex let assert Ok(config) = load_config() // dev-time invariant let assert [first, ..] = required_list // provable elsewhere ``` For real user input, return `Result` and match properly. ## Reading order for the reference [Section titled “Reading order for the reference”](#reading-order-for-the-reference) If you want the full rulebook: * [Pattern Matching reference](/reference/pattern-matching/): the authoritative list of pattern forms. * [Sum Types reference](/reference/sum-types/): how variants are declared and what you can destructure. * [Control Flow reference](/reference/control-flow/): how `match` fits alongside `if` and `for..in`.
---
# Building for Salesforce
> A worked example using SObjects, DML, @AuraEnabled, and sharing modifiers in one project.
This guide walks through building a small but realistic Salesforce feature in Vertex: a function that creates a new Account, tags it with a classification, and exposes the operation to a Lightning component. Along the way it pulls together every Salesforce-specific construct Vertex offers. Before you start, make sure your project is set up per [Your first Salesforce project](/getting-started/first-salesforce-project/). ## Define the SObject [Section titled “Define the SObject”](#define-the-sobject) src/accounts/types.vtx ```vertex @SObject pub type Account { Account( Name: String, Industry: Option, Rating: Option, ) } ``` Three things are happening here: 1. `@SObject` tells the Apex codegen this type is an Account record. Every `@SObject` type also gets an implicit `Id: Option` field. 2. `Industry` and `Rating` are `Option` so they can be omitted at construction time: constructor fields of an `Option` type default to `None`. 3. `pub` exports the type from the module so other Vertex files can import it. ## Core business logic [Section titled “Core business logic”](#core-business-logic) src/accounts/classify.vtx ```vertex import accounts.types type ClassifyError { InsertFailed(reason: String) MissingName } pub fn create_classified_account( name: String, industry: String, ): Result { if name.trim() == "" { Error(MissingName) } else { let acc = types.Account( Name: name, Industry: Some(industry), Rating: Some(pick_rating(industry)), ) match Database.insert(acc) { Ok(id) => Ok(id), Error(dmlErr) => Error(InsertFailed(dmlErr.message)), } } } fn pick_rating(industry: String): String { match industry { "Technology" | "Healthcare" => "Hot", "Retail" => "Warm", _ => "Cold", } } ``` Notes on this file: * **Error types are explicit.** Callers know at compile time what can go wrong. * **`Database.insert` returns `Result`.** We translate the generic `DmlError` into our own domain error. * **`pick_rating` is private.** No `pub` means it is module-local. ## Expose it to Lightning [Section titled “Expose it to Lightning”](#expose-it-to-lightning) src/accounts/aura.vtx ```vertex import accounts.classify @AuraEnabled pub type ClassifyResult { ClassifyResult(accountId: String, success: Bool) } @AuraEnabled pub fn classify_for_lwc( name: String, industry: String, ): Result { match classify.create_classified_account(name: name, industry: industry) { Ok(id) => Ok(ClassifyResult(accountId: id.toString(), success: true)), Error(classify.MissingName) => Error(AuraError("Name is required")), Error(classify.InsertFailed(msg)) => Error(AuraError("Could not save: ${msg}")), } } ``` Notes: * **`@AuraEnabled` on the type** makes every field serializable to LWC. * **`@AuraEnabled` on the function** exposes it, and the return type must be `Result<_, AuraError>`. `Ok` becomes the value passed to LWC; `Error` becomes an `AuraHandledException` with your message. * **The boundary is narrow.** The rich `ClassifyError` lives inside Vertex; the LWC sees only a single-line message. ## Sharing and security [Section titled “Sharing and security”](#sharing-and-security) By default, every Vertex module compiles to `public with sharing`, the safe Salesforce default. To opt into a different sharing mode, add an annotation at the top of the file: src/accounts/admin\_job.vtx ```vertex @WithoutSharing import accounts.classify // ... ``` You almost never want this. Use it only when you have a specific reason (a scheduled job that needs to see all records) and audit the code accordingly. ## Building and deploying [Section titled “Building and deploying”](#building-and-deploying) From the project root: ```plaintext $ vertex build Success! Built 3 class(es) to force-app/main/default/classes/ ``` ```plaintext $ sf project deploy start --target-org ``` `sf` tracks local changes on its own; do not pass `--source-dir` or `--metadata`. ## Testing before deploying [Section titled “Testing before deploying”](#testing-before-deploying) src/accounts/classify\_test.vtx ```vertex import accounts.classify import accounts.types @Test fn rejects_blank_name(): Void { let result = classify.create_classified_account(name: " ", industry: "Retail") let assert Error(classify.MissingName) = result } @Test fn returns_id_on_success(): Void { let result = classify.create_classified_account(name: "Acme", industry: "Retail") let assert Ok(_) = result } ``` Run: ```plaintext $ vertex test ✓ rejects blank name ✓ returns id on success 2 passed, 0 failed (8ms) ``` The second test exercises `Database.insert` without a real org. In JIT mode, the DML layer is simulated and `Database.insert` returns a stub `Id`. The test verifies *your code* does the right thing; org-side behaviour is verified with an actual deploy. ## Calling from LWC [Section titled “Calling from LWC”](#calling-from-lwc) In your Lightning component: ```javascript import classifyForLwc from '@salesforce/apex/Aura.vtx_classify_for_lwc'; classifyForLwc({ name: 'Acme', industry: 'Retail' }) .then(result => { /* { accountId, success } */ }) .catch(error => { /* AuraHandledException message */ }); ``` The Apex method name is `vtx_` + the Vertex function name. ## What to remember [Section titled “What to remember”](#what-to-remember) 1. **Keep domain logic in pure Vertex modules.** `@SObject` types and `Database.*` calls are fine, but keep `@AuraEnabled` boundaries thin. 2. **Error types communicate the API contract.** Don’t collapse them to strings until you are exiting the Vertex world. 3. **Test before you deploy.** The simulated DML layer covers most of your code; a quick deploy covers the rest. 4. **Default to `with sharing`.** Only change it with a reason. ## See it in a real project [Section titled “See it in a real project”](#see-it-in-a-real-project) [vertex-run/vertex-library-management](https://github.com/vertex-run/vertex-library-management) is a sample app that applies the patterns in this guide end to end: `@SObject` types, `Database.*` through `Result`, `@AuraEnabled` controllers, and a `domain/` module with tests that run on the JIT. Browse the source in [`src/`](https://github.com/vertex-run/vertex-library-management/tree/main/src).
---
# Testing your code
> Writing @Test functions, structuring test files, and testing Salesforce code without an org.
Vertex tests run locally. You do not need a Salesforce org to iterate on tests, not even for DML-heavy code. This guide covers the day-to-day workflow. For the definitive rules, see [the Testing reference](/reference/testing/). ## Your first test [Section titled “Your first test”](#your-first-test) Create a file whose name ends in `_test.vtx`: invoice\_test.vtx ```vertex import billing.invoice @Test fn totals_include_tax(): Void { let inv = invoice.new_line_item(100) assert invoice.total_with_tax(inv: inv, tax_rate: 10) == 110 } ``` Run it: ```plaintext $ vertex test src/billing/invoice_test.vtx ✓ totals include tax 1 passed, 0 failed (12ms) ``` Or discover and run everything: ```plaintext $ vertex test ``` ## Structure tests around behaviour, not methods [Section titled “Structure tests around behaviour, not methods”](#structure-tests-around-behaviour-not-methods) A test name reads best as a full sentence. The compiler does not care; your future self does. ```vertex // Good @Test fn totals_include_tax(): Void { ... } @Test fn zero_line_invoice_is_rejected(): Void { ... } // Less good @Test fn test_calculate_total_1(): Void { ... } ``` Group tests by the behaviour they describe, not by the function they happen to call. ## Assertions [Section titled “Assertions”](#assertions) Use `assert` for individual checks. The failure message includes the expression and the runtime value: ```vertex @Test fn adds_two_numbers(): Void { assert 1 + 1 == 2 assert "hello".length() == 5 } ``` For checks you want to phrase as a one-liner with a richer message, fall back to pattern assertions: ```vertex @Test fn user_is_found(): Void { let assert Some(user) = find_user(id: "42") assert user.name == "Alice" } ``` ## DML-heavy tests without an org [Section titled “DML-heavy tests without an org”](#dml-heavy-tests-without-an-org) In JIT mode, DML operations are simulated: * `Database.insert(sobj)` returns `Ok(stub_id)` and prints a notice to stderr. * `Database.update`, `delete`, and `undelete` also return `Ok(...)`. Your logic runs the same way it would on an org, but without the latency and without touching real data. ```vertex @Test fn creates_and_updates_in_sequence(): Void { let acc = Account(Name: "Test Co") let assert Ok(id) = Database.insert(acc) let updated = Account(Name: "Renamed", Id: Some(id)) let assert Ok(_) = Database.update(updated) } ``` The assertion tells you *your code* does the right sequence of calls; it does not tell you the org accepted them. When that matters, run the same test on the org via [the build+deploy flow](/guides/salesforce-projects/). ## Filtering during development [Section titled “Filtering during development”](#filtering-during-development) ```plaintext $ vertex test --filter invoice ``` Runs only tests whose names contain `invoice`. Useful while you are iterating on one bug. ## Test modules are separate from production modules [Section titled “Test modules are separate from production modules”](#test-modules-are-separate-from-production-modules) The compiler enforces it: non-test modules may not import test modules. This keeps your test helpers out of the Apex classes that `vertex build` emits. Production imports test: compile error. ## What is not yet supported [Section titled “What is not yet supported”](#what-is-not-yet-supported) * **Mocking Apex FFI calls.** `extern`-declared functions do not run in JIT mode at all. Tests that need an Apex API must either avoid it in the unit under test or run on the org. * **Test fixtures.** There is no before/after hook yet; each `@Test` function is self-contained. Factor out a helper function when the setup is shared. * **Parameterised tests.** There is no built-in table-driven test support. A `for n in cases { assert check(n) }` loop is an acceptable substitute. See the [Testing reference](/reference/testing/) for the exact rules enforced by the checker.
---
# Annotations
> Annotations in Vertex, @Test for testing, AuraEnabled, SObject, sharing modifiers.
## `@Test` on functions [Section titled “@Test on functions”](#test-on-functions) Marks a zero-parameter function as a test case. The `vertex test` command discovers and runs all `@Test` functions in `*_test.vtx` files. ```vertex @Test fn add_two_numbers(): Void { assert 1 + 1 == 2 } ``` Rules enforced by the checker: * The function must have **no parameters** (error E136). * `@Test` may only appear in files whose name ends in `_test.vtx` (error E137). * Non-test modules may not import test modules (error E138). `@Test` functions are exempt from the unused-private-function warning (W005) because the test runner calls them externally. See the [Testing reference](/reference/testing/) for the full testing guide. ## Module-level sharing annotations [Section titled “Module-level sharing annotations”](#module-level-sharing-annotations) Three annotations control the Apex sharing modifier on the generated outer class. They are placed at the very top of a `.vtx` file, before any `import` or declaration: ```vertex @WithoutSharing import lang.db pub fn run(): Void { db.insertAccount(...) } ``` | Annotation | Generated Apex class header | | ------------------- | ---------------------------------------------------- | | *(none)* | `public with sharing class Module_Name { ... }` | | `@WithSharing` | `public with sharing class Module_Name { ... }` | | `@WithoutSharing` | `public without sharing class Module_Name { ... }` | | `@InheritedSharing` | `public inherited sharing class Module_Name { ... }` | **Default is `with sharing`**. This is the safe default per Apex security best practices and aligns with PMD’s `ApexSharingViolations` rule. Rules: * At most one sharing annotation per module. A second one is an error (`E141`). * Sharing annotations take no arguments. Arguments are an error (`E142`). * Unknown module-level annotation names are an error (`E140`). * Sharing annotations are a no-op in JIT mode and in anonymous-Apex paths. * Inner classes inherit sharing from the outer class. ## `@AuraEnabled` on functions [Section titled “@AuraEnabled on functions”](#auraenabled-on-functions) Marks a function as callable from Lightning components. The function must return `Result`. The codegen unwraps the `Result` at the Apex boundary: `Ok(value)` returns `value` directly; `Error(AuraError(msg))` throws `new AuraHandledException(msg)`. ```vertex @AuraEnabled fn getContacts( ): Result, AuraError> { ... } @AuraEnabled(cacheable: true) fn getAccountName( id: String, ): Result { ... } ``` ## `@AuraEnabled` on types [Section titled “@AuraEnabled on types”](#auraenabled-on-types) Marks a single-variant record type as an LWC data contract. The codegen emits `@AuraEnabled` on every field and drops `final` from fields (required for LWC serialization/deserialization). The type must be a single-variant record (variant name matches the type name). Applying `@AuraEnabled` with arguments or to a multi-variant type is an error. ```vertex @AuraEnabled pub type BookDto { BookDto(name: String, authorName: String, price: Decimal) } @AuraEnabled pub fn getBook(id: String): Result { ... } ``` Generates: ```apex public class BookDto { @AuraEnabled public String name; @AuraEnabled public String authorName; @AuraEnabled public Decimal price; public BookDto(String name, String authorName, Decimal price) { this.name = name; this.authorName = authorName; this.price = price; } } ```
---
# Assertions
> Runtime invariant checks with assert.
`assert` is a **statement** that checks a Boolean condition at runtime. If the condition is `true`, execution continues silently. If it is `false`, the program panics immediately with a descriptive failure message. ```vertex let x = 42 assert x > 0 // passes. Silent assert x == 42 // passes. Silent ``` ## Syntax [Section titled “Syntax”](#syntax) ```plaintext assert ``` The expression must be of type `Bool`. The checker rejects any other type at compile time (error `E100`). ```vertex assert true // ok assert 1 == 1 // ok assert "hello" // E100: type mismatch. Expected Bool, found String ``` ## Failure messages [Section titled “Failure messages”](#failure-messages) When an assertion fails, Vertex prints a rich message that includes: * The source location (file, line, column). * The rendered source of the condition. * For comparison operators (`==`, `!=`, `<`, `>`, `<=`, `>=`): the actual values of the left-hand and right-hand sides. **Comparison failure example:** ```plaintext assertion failed at examples/jit/lang/assertions.vtx:7:1 source: x == 42 left: 5 right: 42 ``` **Non-comparison failure example:** ```plaintext assertion failed at examples/jit/lang/assertions.vtx:11:1 source: list.contains(target) ``` ## `assert` vs `let assert` [Section titled “assert vs let assert”](#assert-vs-let-assert) These are two distinct constructs that share the `assert` keyword: | | `assert` | `let assert` | | --------------- | ---------------------- | --------------------------------------------- | | Purpose | Check a Bool invariant | Destructure a value that must match a pattern | | Form | `assert expr` | `let assert Pat = expr` | | Failure message | `assertion failed: …` | `let assert pattern did not match: …` | Both cause a panic on failure and are appropriate during development and in tests. ## Apex codegen [Section titled “Apex codegen”](#apex-codegen) `assert expr` compiles to `System.Assert.isTrue(expr, 'assertion failed at …')`. The message is a compile-time string. No runtime decomposition occurs on the Apex side. Assertions are **never stripped** from emitted Apex; there is no release-build mode that removes them.
---
# Bindings and constants
> let, mutable let, and const in Vertex, plus scoping and shadowing rules.
Vertex has three binding forms: `let` (immutable local), `mutable let` (mutable local), and `const` (module-level compile-time constant). ## `let`: immutable local bindings [Section titled “let: immutable local bindings”](#let-immutable-local-bindings) ```vertex let name = "Alice" // immutable; cannot be reassigned let count: Int = 42 // optional explicit type annotation ``` Reassigning an immutable binding is a compile-time error (`E101`). Type annotations are optional when the type can be inferred from the initializer. ### Scoping [Section titled “Scoping”](#scoping) `let` bindings are lexically scoped. They are visible from the point of declaration to the end of the enclosing block. ```vertex fn compute(x: Int): Int { let doubled = x * 2 // visible for the rest of compute if doubled > 100 { let big = true // visible only inside this if-block debug big } // 'big' is not visible here doubled } ``` ### Shadowing [Section titled “Shadowing”](#shadowing) Declaring a new `let` with a name already in scope shadows the outer binding for the remainder of the block. Shadowing is allowed, common, and often preferred to mutation. ```vertex let x = "42" let x = Int.parse(x) // a new binding; outer x is shadowed ``` ### Unused-binding warnings [Section titled “Unused-binding warnings”](#unused-binding-warnings) If you declare a `let` you do not use, the compiler emits `W002`. To silence the warning on a binding you genuinely do not need, prefix the name with `_`: ```vertex let _unused = compute() // no warning ``` ## `mutable let`: mutable local bindings [Section titled “mutable let: mutable local bindings”](#mutable-let-mutable-local-bindings) ```vertex mutable let total = 0 total = total + 1 total = total + 1 debug total // 2 ``` Reassignment is allowed only on `mutable let`. The `mutable` keyword is part of the declaration, not a modifier on later assignments; every reassignment without that keyword on the original declaration is a compile error. `mutable` applies to the binding, not to the value: you cannot “mutate” an immutable list via a mutable binding. Collections are themselves immutable; mutation happens by binding a new value. ## `const`: module-level constants [Section titled “const: module-level constants”](#const-module-level-constants) Module-level constants are declared with `const`. Their right-hand side is evaluated at compile time, so only compile-time-constant expressions are allowed: scalar literals, arithmetic on constants, collection literals of constants, and references to other constants in the same module. ```vertex const max_retries = 3 const api_version: String = "v2" pub const discount_rate = 10 // visible to importers ``` ### Scalar literals [Section titled “Scalar literals”](#scalar-literals) Any of `Int`, `Long`, `Double`, `Decimal`, `Bool`, or `String` may appear on the right-hand side directly. ```vertex const enabled = true const pi: Double = 3.14 const greeting = "Hello" ``` String interpolation is not permitted in constant strings. ### Arithmetic and references [Section titled “Arithmetic and references”](#arithmetic-and-references) Binary `+`, `-`, `*`, `/`, `%` are allowed on numeric constants. `+` on two `String` operands performs concatenation. Unary minus is allowed. A constant may reference any other `const` in the same module, even one declared later. Cycles are an error. ```vertex const doubled = base * 2 // forward reference: fine const base = 10 ``` ### Collection literals [Section titled “Collection literals”](#collection-literals) Lists, maps, and sets of constants can be `const` values. ```vertex const statuses = ["Draft", "Active", "Closed"] const codes = {200: "OK", 404: "Not Found"} const flags = #{true, false} ``` Empty collections need an explicit type annotation so the element type is known. ```vertex const empty: List = [] ``` ### Visibility [Section titled “Visibility”](#visibility) * **Private** (`const ...`): accessible within the same module only. * **Public** (`pub const ...`): exported in the module’s signature and accessible via `module.name`. Selective import of a `pub const` by name is not yet supported. Use a bare `import foo` and qualify with `foo.constant`. ### Codegen [Section titled “Codegen”](#codegen) * **Class-file Apex.** Emits as `[public|private] final static Type name = value;`. * **Anonymous Apex.** Emits as `final Type name = value;` at the top of the block, before any body statements. The emitted value is always the fully-evaluated compile-time literal, never a reference or expression. ## Picking the right binding [Section titled “Picking the right binding”](#picking-the-right-binding) A rule of thumb: * If the value does not change: `let`. * If the value changes during the function: `mutable let`, only after ruling out a rewrite that avoids mutation. * If the value is a module-level configuration shared across functions: `const`.
---
# Collections
> List, Map, and Set collection types in Vertex.
## `List` [Section titled “List\”](#listt) ```vertex let names: List = ["Alice", "Bob", "Carol"] let empty: List = [] ``` An empty list `[]` requires a type annotation whenever the element type cannot be determined from context: ```vertex let empty: List = [] // OK. Annotation provides T = String let empty = [] // error: cannot infer element type ``` When an empty list is passed directly to a function whose parameter type is known, the annotation can be omitted: ```vertex fn greet(names: List): Void { ... } greet([]) // OK, T inferred from the parameter type ``` Spread syntax combines lists: ```vertex let combined = [...list1, ...list2, "extra"] ``` `...expr` (three dots) is the spread form and only appears in list *expressions*. The two-dot `..` is a pattern-only form for silently discarding the rest of a list or the unmentioned fields of a variant. See [Pattern Matching](/reference/pattern-matching/). `..` is never valid inside a list literal. Lists can be iterated with `for..in` and pattern-matched with list patterns. There is no subscript syntax (`list[i]`). Use `get(i)` for index access. ### Instance methods [Section titled “Instance methods”](#instance-methods) | Method | Signature | Returns | Notes | | ------------------------------------- | --------------- | ----------------- | --------------------------------------------------------------------- | | `size()` | `List` | `Int` | Number of elements | | `isEmpty()` | `List` | `Bool` | `true` when size is 0 | | `get(i: Int)` | `List` | `Option` | `None` on out-of-bounds; no panic | | `add(item: T)` | `List` | `List` | Pure. Returns a new list with `item` appended | | `first()` | `List` | `Option` | `None` on empty list | | `last()` | `List` | `Option` | `None` on empty list | | `contains(item: T)` | `List` | `Bool` | Value equality for primitives; reference equality for class instances | | `map(f: fn(T): U)` | `List` | `List` | Pure. Returns a new list | | `filter(f: fn(T): Bool)` | `List` | `List` | Pure. Returns a new list | | `fold(init: U, f: fn(U, T): U)` | `List` | `U` | Accumulator-first callback convention | | `any(f: fn(T): Bool)` | `List` | `Bool` | Short-circuits on first match; `false` on empty | | `all(f: fn(T): Bool)` | `List` | `Bool` | Short-circuits on first non-match; `true` on empty (vacuous truth) | | `enumerate()` | `List` | `List<#(Int, T)>` | Pairs each element with its zero-based index | | `reverse()` | `List` | `List` | Pure. Returns a new list in reversed order | | `prepend(item: T)` | `List` | `List` | Pure. Returns a new list with `item` at the front | | `concat(other: List)` | `List` | `List` | Pure. Returns a new list with `other` appended | | `take(n: Int)` | `List` | `List` | Pure. Returns the first `n` elements; `n ≤ 0` returns `[]` | | `drop(n: Int)` | `List` | `List` | Pure. Skips the first `n` elements; `n ≤ 0` returns a copy | | `unique()` | `List` | `List` | Pure. Removes duplicates, preserving first-occurrence order | | `toSet()` | `List` | `Set` | Converts to a `Set`, removing duplicates | | `flatten()` | `List>` | `List` | Concatenates all inner lists into a single list | | `join(sep: String)` | `List` | `String` | Joins all strings with `sep` | | `filterMapSome(f: fn(T): Option)` | `List` | `List` | Applies `f`, keeps only `Some` values, discards `None` | | `filterMapOk(f: fn(T): Result)` | `List` | `List` | Applies `f`, keeps only `Ok` values, discards errors | | `indexOf(item: T)` | `List` | `Option` | Returns `Some(index)` of first match, or `None` | | `firstWhere(f: fn(T): Bool)` | `List` | `Option` | Returns `Some(first element)` where `f` is true, or `None` | ```vertex let xs: List = [1, 2, 3, 4, 5] let doubled = xs.map(fn(n) { n * 2 }) // (2, 4, 6, 8, 10) let evens = xs.filter(fn(n) { n % 2 == 0 }) // (2, 4) let sum = xs.fold(0, fn(acc, n) { acc + n }) // 15 let hasOdd = xs.any(fn(n) { n % 2 != 0 }) // true let allPos = xs.all(fn(n) { n > 0 }) // true // Safe index access let assert Some(value: third) = xs.get(2) // third == 3 let missing = xs.get(99) // None // enumerate: iterate with index let words: List = ["a", "b", "c"] for #(i, w) in words.enumerate() { debug i // 0, 1, 2 } ``` ### Factory functions [Section titled “Factory functions”](#factory-functions) | Signature | Returns | Notes | | ------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `List.generate(count: Int, f: fn(Int): T)` | `List` | Calls `f` with each zero-based index `0..count-1`. `count ≤ 0` returns `[]`. | | `List.filled(count: Int, value: T)` | `List` | Creates `count` copies of `value`. `count ≤ 0` returns `[]`. Note: all entries reference the same object. Use `List.generate` for independent instances. | ```vertex // Unique items using the index: let squares: List = List.generate(5, fn(i: Int): Int { i * i }) // squares == (0, 1, 4, 9, 16) // Repeated sentinel value: let zeros: List = List.filled(4, 0) // zeros == (0, 0, 0, 0) ``` *** ## `Map` [Section titled “Map\”](#mapk-v) ```vertex let scores: Map = {"Alice": 95, "Bob": 87} let empty: Map = Map.empty() ``` There is no subscript syntax (`map["key"]`). Use `get(key)` for key access, which returns `Option`. ### Instance methods [Section titled “Instance methods”](#instance-methods-1) | Method | Receiver | Returns | Notes | | ----------------------- | ---------- | --------------- | ---------------------------------------- | | `size()` | `Map` | `Int` | Number of key-value pairs | | `isEmpty()` | `Map` | `Bool` | `true` when the map has zero entries | | `containsKey(key: K)` | `Map` | `Bool` | `true` if the key exists | | `get(key: K)` | `Map` | `Option` | `Some(value)` if found, `None` otherwise | | `put(key: K, value: V)` | `Map` | `Map` | Pure. Returns a new map with the key set | | `remove(key: K)` | `Map` | `Map` | Pure. Returns a new map without the key | | `keys()` | `Map` | `List` | All keys as a list | | `values()` | `Map` | `List` | All values as a list | | `entries()` | `Map` | `List<#(K, V)>` | Key-value pairs as a list of tuples | ### Factory functions [Section titled “Factory functions”](#factory-functions-1) `Map.empty()`. Creates an empty map. A type annotation is required: ```vertex let m: Map = Map.empty() ``` `Map.fromList(list: List<#(K, V)>): Map`. Constructs a map from a list of key-value tuple pairs. If the list contains duplicate keys, the **last** entry wins. ```vertex let m: Map = Map.fromList([#("a", 1), #("b", 2)]) // Round-trip with entries(): let original: Map = {"x": 10, "y": 20} let copy: Map = Map.fromList(original.entries()) ``` ### Iteration [Section titled “Iteration”](#iteration) ```vertex for k in scores.keys() { debug k } for #(key, value) in scores.entries() { debug "${key}: ${value}" } ``` *** ## `Set` [Section titled “Set\”](#sett) ```vertex let tags: Set = #{"apex", "salesforce"} ``` Sets are immutable; all mutating methods return a new `Set`. ### Instance methods [Section titled “Instance methods”](#instance-methods-2) | Method | Receiver | Returns | Description | | ----------------------------- | -------- | --------- | ------------------------------------------- | | `size()` | `Set` | `Int` | Number of elements | | `isEmpty()` | `Set` | `Bool` | `true` if the set has no elements | | `contains(item: T)` | `Set` | `Bool` | `true` if `item` is in the set | | `add(item: T)` | `Set` | `Set` | New set with `item` included | | `remove(item: T)` | `Set` | `Set` | New set with `item` excluded | | `toList()` | `Set` | `List` | Converts to a list (order unspecified) | | `map(f: fn(T): U)` | `Set` | `Set` | Transforms each element with `f` | | `filter(f: fn(T): Bool)` | `Set` | `Set` | Keeps elements for which `f` returns `true` | | `union(other: Set)` | `Set` | `Set` | All elements from both sets | | `intersection(other: Set)` | `Set` | `Set` | Elements present in both sets | | `difference(other: Set)` | `Set` | `Set` | Elements in receiver but not in `other` | ```vertex let nums: Set = #{1, 2, 3} debug nums.size() // 3 debug nums.contains(2) // true let s2 = nums.add(4) debug s2.size() // 4 let evens = nums.filter(fn(x: Int): Bool { x % 2 == 0 }) debug evens.size() // 1 let u = #{1, 2}.union(#{2, 3, 4}) debug u.size() // 4 let inter = #{1, 2, 3}.intersection(#{2, 3, 4}) debug inter.size() // 2 let diff = #{1, 2, 3}.difference(#{2, 3}) debug diff.size() // 1 ```
---
# Control Flow
> if/else, if let, block expressions, and return in Vertex.
### `if` / `else`: an expression, produces a value [Section titled “if / else: an expression, produces a value”](#if--else-an-expression-produces-a-value) ```vertex let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "F" } ``` An `if` without `else` is only valid where a `Void` value is acceptable. ### `if let`: conditional variant matching [Section titled “if let: conditional variant matching”](#if-let-conditional-variant-matching) `if let` checks whether a value matches a pattern and, if so, binds the pattern’s variables in the then-block. It is the idiomatic way to handle a single variant without writing an exhaustive `match`. ```vertex // Basic: execute only when the Option is Some if let Some(user) = findUser(42) { debug user } // With else let name = if let Some(user) = findUser(42) { user.name } else { "anonymous" } // Works for any sum type, not just Option/Result type ApiResponse { Success(data: String), Loading, Failed(error: String) } if let Success(data:) = response { process(data) } ``` ```vertex // Chaining if let Circle(radius: r) = shape { debug r } else if let Square(side: s) = shape { debug s } else { debug "other" } ``` **Scoping:** Pattern bindings are scoped to the **then-block only**. They are not visible in the `else` branch or after the `if let` expression. **When to use each construct:** | Construct | Intent | On mismatch | | ------------ | ------------------------------------ | ----------------- | | `match` | Handle **all** variants | N/A (exhaustive) | | `if let` | Conditionally handle **one** variant | Branches or skips | | `let assert` | Assert a specific variant | **Panics** | **Valid patterns:** `VariantPat`, `TuplePat`, `IdentPat`, `WildcardPat`. `LiteralPat`, `ListPat`, and `OrPat` are not valid (use `match` for those). ### Block expressions [Section titled “Block expressions”](#block-expressions) A block’s value is its trailing expression. Without a trailing expression it evaluates to `Void`. ```vertex let result = { let x = 5 x * x } ``` ### `return`: early exit from a function [Section titled “return: early exit from a function”](#return-early-exit-from-a-function) ```vertex fn divide( a: Int, b: Int, ): Result { if b == 0 { return Error("division by zero") } Ok(a / b) } ```
---
# Compiler diagnostics
> Every error and warning code the Vertex compiler emits, with an example and a fix.
Vertex takes diagnostics seriously. Every error has a title in plain English, notes about what the compiler knows, an actionable `help:` section, and where relevant a second span pointing at the conflicting declaration. This page is the index: each entry links the error code to an example and a one-line fix. For the format philosophy, see [Why Vertex: errors that teach](/why-vertex/#5-errors-that-teach). ## Errors [Section titled “Errors”](#errors) ### `E100`: Type mismatch [Section titled “E100: Type mismatch”](#e100-type-mismatch) The expression’s inferred type does not match what the context expects. ```vertex let n: Int = "hello" ``` **To fix:** convert the value or change the declaration. If you meant the declaration to match the value, drop the annotation and let the compiler infer. ### `E101`: Assignment to an immutable binding [Section titled “E101: Assignment to an immutable binding”](#e101-assignment-to-an-immutable-binding) You tried to reassign a `let`. ```vertex let x = 0 x = 1 ``` **To fix:** change `let` to `mutable let`, or rebind with a new name: `let y = 1`. ### `E102`: Undefined name [Section titled “E102: Undefined name”](#e102-undefined-name) The name is not in scope at the point of use. ```vertex debug grand_total ``` **To fix:** declare the binding or check the spelling. If the name lives in another module, add an `import`. ### `E103`: Wrong number of arguments [Section titled “E103: Wrong number of arguments”](#e103-wrong-number-of-arguments) The call passes too many or too few arguments. **To fix:** match the function’s parameter list, including any required named parameters. ### `E104`: Pattern not valid in this position [Section titled “E104: Pattern not valid in this position”](#e104-pattern-not-valid-in-this-position) A pattern form is not allowed in the context (for example, an OR pattern that binds different shapes). **To fix:** split the pattern into separate arms, or use a wildcard and match on fields inside the arm body. ### `E105`: Non-exhaustive `match` [Section titled “E105: Non-exhaustive match”](#e105-non-exhaustive-match) One or more variants of the matched type are not covered. ```vertex type Status { Active, Inactive, Archived } fn label(s: Status): String { match s { Active => "active", Inactive => "inactive", } } ``` **To fix:** add an arm for the missing variant, or a `_` wildcard if you genuinely mean “anything else”. The compiler message lists the missing variants by name. ### `E106`: `?` applied to a non-`Result` [Section titled “E106: ? applied to a non-Result”](#e106--applied-to-a-non-result) `?` only works on `Result` expressions. **To fix:** return a `Result` from the subexpression, or match on it directly if it is an `Option`. ### `E107`: Error types don’t align in `?` propagation [Section titled “E107: Error types don’t align in ? propagation”](#e107-error-types-dont-align-in--propagation) The enclosing function’s error type does not match the inner expression’s error type. **To fix:** convert with `.mapError(...)` before `?`. See the [error handling guide](/guides/error-handling/#crossing-error-type-boundaries). ### `E108`: Class does not implement all interface methods [Section titled “E108: Class does not implement all interface methods”](#e108-class-does-not-implement-all-interface-methods) A type declared to implement an interface is missing one or more methods. **To fix:** implement the missing methods listed by the compiler. ### `E109`: Name declared twice [Section titled “E109: Name declared twice”](#e109-name-declared-twice) A name was declared twice in the same scope. **To fix:** rename one of them, or delete the duplicate. ### `E110`: `...expr` spread on a non-`List` [Section titled “E110: ...expr spread on a non-List”](#e110-expr-spread-on-a-non-list) The spread operator `...` inside a list literal was applied to a value that is not a `List`. **To fix:** convert to a list first, or use a regular element expression. ### `E111`: Method does not exist on this type [Section titled “E111: Method does not exist on this type”](#e111-method-does-not-exist-on-this-type) The method name is not defined for the receiver’s type. **To fix:** check the spelling, or consult the reference for the available methods on the type. ### `E136`: `@Test` function must take no parameters [Section titled “E136: @Test function must take no parameters”](#e136-test-function-must-take-no-parameters) A function marked `@Test` has one or more parameters. **To fix:** remove the parameters, or move the data into the test body. ### `E137`: `@Test` only in `_test.vtx` files [Section titled “E137: @Test only in \_test.vtx files”](#e137-test-only-in-_testvtx-files) A function marked `@Test` is in a non-test file. **To fix:** rename the file to end in `_test.vtx`, or remove the annotation. ### `E138`: Non-test module imports a test module [Section titled “E138: Non-test module imports a test module”](#e138-non-test-module-imports-a-test-module) A non-test module cannot depend on a test module. **To fix:** move the imported symbol to a non-test module, or remove the import. ### `E140`: Unknown module-level annotation [Section titled “E140: Unknown module-level annotation”](#e140-unknown-module-level-annotation) The file starts with an annotation that the compiler does not recognise as a module-level annotation. **To fix:** check the spelling against [Annotations](/reference/annotations/), or move the annotation to the appropriate declaration. ### `E141`: Multiple sharing annotations [Section titled “E141: Multiple sharing annotations”](#e141-multiple-sharing-annotations) A module has more than one sharing annotation. **To fix:** keep at most one of `@WithSharing`, `@WithoutSharing`, or `@InheritedSharing`. ### `E142`: Sharing annotation takes arguments [Section titled “E142: Sharing annotation takes arguments”](#e142-sharing-annotation-takes-arguments) A sharing annotation was called with parentheses or arguments. **To fix:** remove the arguments. Sharing annotations are bare. ### `E203`: Reassignment of `let` [Section titled “E203: Reassignment of let”](#e203-reassignment-of-let) Deeper form of E101 with the original declaration’s location as a related span. **To fix:** same as E101. Use `mutable let` on the original declaration. ## Warnings [Section titled “Warnings”](#warnings) ### `W001`: `debug` in source [Section titled “W001: debug in source”](#w001-debug-in-source) A `debug` expression was left in source code. **To fix:** remove it before shipping, or accept the warning if this is a `vertex run` script. ### `W002`: Unused local binding [Section titled “W002: Unused local binding”](#w002-unused-local-binding) A local `let` was declared but its value is never read. **To fix:** remove the binding, or prefix it with `_` if the side effect of the RHS is what you wanted (`let _ = compute()`). ### `W003`: Unused parameter [Section titled “W003: Unused parameter”](#w003-unused-parameter) A function or closure parameter is never used. **To fix:** remove the parameter, or prefix the name with `_`. ### `W004`: `mutable let` never reassigned [Section titled “W004: mutable let never reassigned”](#w004-mutable-let-never-reassigned) A binding was declared `mutable let` but never reassigned. **To fix:** drop the `mutable` keyword. ### `W005`: Private function never called [Section titled “W005: Private function never called”](#w005-private-function-never-called) A private (non-`pub`) top-level function is never referenced. **To fix:** remove it, mark it `pub` if it is part of the public API, or prefix the name with `_`. ### `W006`: Private type or alias never used [Section titled “W006: Private type or alias never used”](#w006-private-type-or-alias-never-used) A private type declaration is never referenced. **To fix:** remove it, or mark it `pub`. ### `W007`: Private constant never used [Section titled “W007: Private constant never used”](#w007-private-constant-never-used) A private `const` is never referenced. **To fix:** remove it, or mark it `pub const`. ### `W008`: Variant never constructed [Section titled “W008: Variant never constructed”](#w008-variant-never-constructed) A sum-type variant of an internally-constructed type is never created in source. **To fix:** remove the variant, or use it. If a future call site is planned, leave a comment; the warning stays until the variant is constructed. ### `W009`: Imported name never used [Section titled “W009: Imported name never used”](#w009-imported-name-never-used) A name imported selectively is never used. **To fix:** remove the import, or use the name. ## Silencing unused-declaration warnings [Section titled “Silencing unused-declaration warnings”](#silencing-unused-declaration-warnings) Prefix a name with `_` to opt out of W002 through W007. This is the convention for intentionally-unused bindings. ```vertex fn process(_ignored: String): Int { 42 } // W003 suppressed let _temp = expensive_computation() // W002 suppressed fn _helper(): Void { } // W005 suppressed ``` The underscore convention does not apply to selective imports (W009): the imported name is defined elsewhere. Remove unused imports instead.
---
# for..in Loop
> Iterating over a list with for..in in Vertex.
`for..in` is Vertex’s iteration statement. It walks a `List`, binding each element to a pattern. ```vertex for n in [1, 2, 3] { debug n } ``` There is no `while` statement. Iteration is expressed with `for..in`, recursion, or collection combinators (`map`, `filter`, `fold`). ## Over a list [Section titled “Over a list”](#over-a-list) ```vertex let names = ["Alice", "Bob", "Carol"] for name in names { debug "hello, ${name}" } ``` The loop variable is immutable inside the body. To accumulate a result across iterations, declare a `mutable let` outside the loop, or prefer `fold`: ```vertex // Imperative style mutable let total = 0 for n in [1, 2, 3] { total = total + n } debug total // 6 ``` ```vertex // Functional style, no mutation let total = [1, 2, 3].fold(0, fn(acc: Int, n: Int): Int { acc + n }) debug total // 6 ``` ## Destructuring in the loop head [Section titled “Destructuring in the loop head”](#destructuring-in-the-loop-head) The loop head accepts any pattern, so you can destructure tuples, records, or variants without an intermediate binding: ```vertex // Tuple destructuring for #(index, value) in pairs { debug "${index}: ${value}" } // Variant destructuring (matches only shapes of this variant) for Circle(radius: r) in shapes { debug "radius ${r}" } // Wildcard for _ in items { counter = counter + 1 } ``` Caution A variant pattern in the loop head only binds successfully when every element of the list matches that variant. Mixing variants means skipping elements that do not match; use `match` inside the loop if you need to dispatch per-element: ```vertex for shape in shapes { match shape { Circle(radius: r) => ..., Rectangle(width: w, height: h) => ..., } } ``` ## Iterating with an index [Section titled “Iterating with an index”](#iterating-with-an-index) `List.enumerate()` pairs each element with its zero-based index, returning a `List<#(Int, T)>` you can destructure: ```vertex for #(i, name) in names.enumerate() { debug "${i}. ${name}" } ``` ## Why no `for..in` over Map or Set? [Section titled “Why no for..in over Map or Set?”](#why-no-forin-over-map-or-set) Iteration order over a `Map` or `Set` is not guaranteed. Convert the collection to a list first to make the order explicit: ```vertex for #(k, v) in entries.toList() { ... } // Map.toList(): List<#(K, V)> for item in set.toList() { ... } // Set.toList(): List ``` This keeps the iteration order visible in the code rather than depending on hash-order surprises. ## Codegen [Section titled “Codegen”](#codegen) `for..in` emits as a Java-style `for (T name : collection) { ... }` in the generated Apex, with pattern destructuring expanded into the loop body.
---
# Functions
> Module-level functions, closures, higher-order functions, and the pipe operator in Vertex.
### Module-level declarations [Section titled “Module-level declarations”](#module-level-declarations) ```vertex fn add( a: Int, b: Int, ): Int { a + b } ``` * The last expression in the body is the return value. * Explicit `return` is for early exits only. * All module-level `fn` declarations are hoisted. Mutual recursion works. * Trailing commas are allowed in parameter lists and argument lists. ### Return type inference [Section titled “Return type inference”](#return-type-inference) The return type annotation is **optional** on private functions. When omitted, the compiler infers it from the body: ```vertex fn add(a: Int, b: Int) { a + b } // inferred: Int fn greet(name: String) { "Hello" } // inferred: String fn noop() { } // inferred: Void ``` **`pub fn` must always have an explicit return type.** This keeps module boundaries explicit and self-documenting: ```vertex pub fn run(): Void { ... } // OK. Explicit pub fn bad() { ... } // Error E135: pub fn missing return type ``` If two private functions with omitted return types reference each other (mutual recursion with both unannotated), the compiler emits **E136** and requires at least one of them to carry an explicit annotation. ### Named arguments at the call site [Section titled “Named arguments at the call site”](#named-arguments-at-the-call-site) ```vertex let r = add(a: 1, b: 2) // named let r = add(1, 2) // positional ``` ### Default parameter values [Section titled “Default parameter values”](#default-parameter-values) ```vertex fn greet( name: String, prefix: String = "Hello", ): String { "${prefix}, ${name}!" } ``` ### Parameter destructuring [Section titled “Parameter destructuring”](#parameter-destructuring) A parameter may be a pattern instead of a plain name. The pattern is bound at the start of the function body, exactly like a `let` destructuring. **Variant / record destructuring:** ```vertex type Point { Point(x: Double, y: Double) } fn sumSquares(Point(x:, y:): Point): Double { x * x + y * y } ``` **Tuple destructuring:** ```vertex fn addPair(#(a, b): #(Int, Int)): Int { a + b } ``` Mixed plain and destructured parameters are allowed: ```vertex fn scale(n: Double, Point(x:, y:): Point): Double { let sum = x + y n * sum } ``` **Restrictions:** * Destructured parameters are **positional only**. They have no call-site label and cannot be passed by name. * Default values (`= expr`) are not allowed on destructured parameters. * Closures do not support parameter destructuring; use a `let` binding inside the body instead. ### Closures / anonymous functions [Section titled “Closures / anonymous functions”](#closures--anonymous-functions) ```vertex let add5 = fn( x: Int, ): Int { x + 5 } fn make_adder( base: Int, ): fn(Int): Int { fn( x: Int, ): Int { base + x } } ``` Closures are first-class values. They capture their enclosing lexical environment. ### Higher-order functions [Section titled “Higher-order functions”](#higher-order-functions) ```vertex fn apply( f: fn(Int): Int, v: Int, ): Int { f(v) } fn compose( f: fn(Int): Int, g: fn(Int): Int, ): fn(Int): Int { fn( x: Int, ): Int { f(g(x)) } } ``` ### Function type aliases [Section titled “Function type aliases”](#function-type-aliases) Use `type` to give a name to a function type: ```vertex type IntOp = fn(Int): Int type Predicate = fn(Int): Bool fn apply_op(f: IntOp, x: Int): Int { f(x) } fn apply_pred(p: Predicate, x: Int): Bool { p(x) } ``` Generic function type aliases are supported: ```vertex type Transform = fn(A): B fn apply(f: Transform, x: Int): String { f(x) } ``` Aliases are **transparent**: `fn(Int): Int` and `IntOp` are interchangeable. Function type aliases are compile-time only. They have no representation in generated Apex. ### `|>` pipe operator [Section titled “|> pipe operator”](#-pipe-operator) Passes the left value as the **first argument** of the right-hand function: ```vertex debug 3 |> double // 6 debug 3 |> double |> inc // 7 debug 4 |> square |> double // 32 ``` Use `_` as a placeholder to control where the piped value lands: ```vertex fn add(a: Int, b: Int): Int { a + b } fn mul(a: Int, b: Int): Int { a * b } debug 2 |> add(_, 3) // 5 . Same as add(2, 3) debug 2 |> add(_, 3) |> mul(10, _) // 50. Mul(10, add(2, 3)) fn greet(greeting: String, name: String): String { greeting + ", " + name + "!" } debug "world" |> greet("Hello", _) // Hello, world! ``` Without `_`, the piped value is prepended as the first argument. With `_`, the piped value replaces each `_` in the argument list and no implicit prepend occurs. ### Generic functions [Section titled “Generic functions”](#generic-functions) ```vertex fn identity( x: T, ): T { x } fn first( items: List, ): Option { ... } ```
---
# Imports
> Vertex's file-based module system and import forms.
Vertex has a file-based module system: each `.vtx` file is a module. Imports are resolved by both `vertex build` (for compiling to Apex classes) and `vertex run` (for local JIT execution and `--sf` anonymous Apex runs). ## Import forms [Section titled “Import forms”](#import-forms) **Bare import**. Access the module’s public symbols via a qualifier: ```vertex import accounts import billing.invoices // nested path: qualifier is last segment let acc = accounts.findById("001") let inv = invoices.create() // qualifier is the last segment ``` **Aliased import**. Choose your own qualifier: ```vertex import billing.invoices as inv let i = inv.create() ``` **Selective import**. Bring specific symbols directly into scope: ```vertex import accounts.{ findById, Account } let acc = findById("001") // bare call, no qualifier needed ``` ## Module path → Apex class name [Section titled “Module path → Apex class name”](#module-path--apex-class-name) The module path maps to an Apex class name: | Module path | Apex class name | | ------------------ | ------------------ | | `accounts` | `Accounts` | | `billing/invoices` | `Billing_Invoices` | | `lang/greeter` | `Lang_Greeter` | The generated outer class always includes an Apex sharing modifier (see [Annotations](/reference/annotations/)). The default is `with sharing`: ```apex public with sharing class Accounts { ... } public with sharing class Billing_Invoices { ... } ``` Cross-module function calls emit as `TargetClass.fnName(args)`. Selectively imported types emit as `TargetClass.TypeName` in Apex. ## `vertex run` and imports [Section titled “vertex run and imports”](#vertex-run-and-imports) `vertex run` supports imports directly. No build step required for local scripts. **No imports → no project needed.** A file with no `import` declarations runs anywhere, just like `dart run` on a standalone Dart file. **With imports → project discovery.** When the entry file contains imports, `vertex run` walks upward from the entry file’s directory looking for `sfdx-project.json`. The directory that contains that file becomes the project root, and `/src/` is the module tree. Only the transitively reachable modules are loaded; unrelated files in `src/` are never touched. ```plaintext my-project/ sfdx-project.json src/ greeter.vtx ← loaded (imported by the script) unrelated.vtx ← skipped scripts/ hello.vtx ← entry file ``` scripts/hello.vtx ```vertex import greeter.{ greet } debug greet("World") ``` ```plaintext vertex run scripts/hello.vtx ``` **`--sf` with imports.** When using `--sf` with an entry script that has imports, the workspace must already be built and deployed before running: ```plaintext vertex build sf project deploy start --target-org vertex run --sf --org scripts/hello.vtx ``` The generated anonymous Apex references the deployed Apex classes directly (e.g. `Greeter.greet("World")`). If a referenced class is not deployed, the org returns a runtime “unknown class” error. **The entry file is a script, not a module.** It may contain top-level statements and a trailing expression. It is excluded from the module graph even if it lives under `src/`. Other modules cannot import the entry file. ## Visibility [Section titled “Visibility”](#visibility) Only `pub` declarations are importable. Calling a non-`pub` symbol from another module is a compile error (`E124`). ## Error codes [Section titled “Error codes”](#error-codes) | Code | Meaning | | ------ | ---------------------------------------------------------------- | | `E122` | Module not found (no matching `.vtx` file) | | `E123` | Symbol not found in module’s public API | | `E124` | Attempted access to a private declaration | | `E125` | Cyclic import detected | | `E126` | Name conflict: same symbol selectively imported from two modules | | `E127` | Variant constructor called on opaque type outside its module | | `E128` | Field accessed on opaque type outside its module | | `E129` | Variant pattern matched on opaque type outside its module | | `E130` | Record update used on opaque type outside its module |
---
# Current limitations
> Constructs that are partially implemented or not yet available.
Caution This page lists constructs that are **parsed but not yet executable**, or **not yet implemented at all**. It reflects the current state of the language. Anything not listed here is expected to work. | Limitation | Why | Workaround | | -------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Record update syntax `...record` | Parser accepts it; runtime throws. | Construct a new record explicitly with the fields you want to change. | | `extern` calls in JIT mode | JIT has no Salesforce runtime. | Run these code paths with `vertex run --sf --org ` or from a deployed Apex test. | | SOQL in Vertex source | SOQL is not yet part of the language. | Wrap the specific queries you need with `extern fn` around `Database.query` / `queryWithBindings`. | | `while` and `do`/`while` statements | Intentional: iteration is `for..in`, recursion, or combinators. | Use `for..in`, `List.fold`, or recursion. | | Mutating methods on collections | All collection operations are pure. | Rebind: `let xs = xs.add(item)`. | | `null` in user code | Intentional: absence is `Option`. | Use `Option` with `Some` / `None`. | | `throw` / `try` / `catch` in user code | Intentional: failure is `Result`. | Return `Result` and propagate with `?`. | | Class inheritance | Intentional: composition over inheritance. | Model shared behaviour with interfaces; share implementation via module-level functions. | | Static methods on types | Intentional: module-level `fn` only. | Put “static” helpers at module level; readers see them as regular functions. | | Parameterised tests | Not yet supported. | Loop inside an `@Test` function over a list of cases. | | Before/after hooks in tests | Not yet supported. | Factor shared setup into a helper function called at the top of each test. | | Formatter | Not yet available. | Follow the examples in the [tour](/tour/01-hello-world/) for idiomatic style. | | Raw string literals | Not yet supported. | Use explicit escape sequences. | | VS Code / JetBrains extensions | Not yet available. | Any LSP-capable editor can drive `vertex lsp`. Zed has a purpose-built extension. | ## When a limitation bites [Section titled “When a limitation bites”](#when-a-limitation-bites) If you hit a limitation in a real project, file an issue at [vertex-run/vertex](https://github.com/vertex-run/vertex/issues) with a minimal reproducing example. The list above is curated; a “thing I tried and it didn’t work” that is not listed is almost always a bug worth reporting.
---
# Opaque Types
> Encapsulated sum types whose internals are hidden from importing modules.
A `pub opaque type` is a sum type whose **constructors, fields, and variant patterns are private to its defining module**. Outside that module the type is visible and can be used in annotations, as function parameters, and as return types, but callers cannot inspect or construct its internal representation directly. ```vertex // in module user_ids.vtx pub opaque type UserId { UserId(value: String) } // Smart constructor. The only public way to create a UserId. pub fn make(s: String): UserId { UserId(value: s) } // Public accessor. Exposes the value without leaking the constructor. pub fn unwrap(uid: UserId): String { match uid { UserId(value: v) => v, } } ``` ### What is restricted outside the defining module [Section titled “What is restricted outside the defining module”](#what-is-restricted-outside-the-defining-module) | Operation | Error | | ------------------------------------------------------ | ------ | | `UserId(value: "x")`. Variant constructor call | `E127` | | `uid.value`. Field access | `E128` | | `match uid { UserId(value: v) => v }`. Variant pattern | `E129` | | Record update syntax on the opaque type | `E130` | ### What is allowed outside the defining module [Section titled “What is allowed outside the defining module”](#what-is-allowed-outside-the-defining-module) * Using the type in annotations: `fn foo(uid: UserId): UserId` * Passing values through: `fn passThrough(uid: UserId): UserId { uid }` * Wildcard match: `match uid { _ => 0 }` * Calling the module’s public smart constructors and accessors ```vertex // in module consumer.vtx import user_ids.{UserId, make, unwrap} fn run(): String { let uid = make("abc") // OK. Uses the smart constructor unwrap(uid) // OK. Uses the public accessor } fn bad(): UserId { UserId(value: "x") } // E127 fn bad2(uid: UserId): String { uid.value } // E128 ``` ### Exhaustiveness [Section titled “Exhaustiveness”](#exhaustiveness) Because variant internals are hidden, `match` on an opaque type from outside its defining module requires a **wildcard arm** (`_ => ...`). Any variant pattern arm will be rejected with `E129`, and a match with no wildcard and no valid arms is non-exhaustive (`E105`). ```vertex // OK. Wildcard covers the opaque type match uid { _ => "some value", } ``` ### Syntax [Section titled “Syntax”](#syntax) `opaque` is only valid in combination with `pub`: ```plaintext pub opaque type Name { ... } // OK opaque type Name { ... } // parse error. Opaque requires pub pub opaque fn foo() {} // parse error. Opaque only applies to types ```
---
# Operators
> Arithmetic, comparison, logical, and unary operators in Vertex.
### Arithmetic [Section titled “Arithmetic”](#arithmetic) | Operator | Supported types | | -------- | ------------------------------------------------------------ | | `+` | `Int`, `Long`, `Double`, `Decimal`, `String` | | `-`, `*` | `Int`, `Long`, `Double`, `Decimal` | | `/` | `Int` (truncating), `Long` (truncating), `Double`, `Decimal` | | `%` | `Int`, `Long` | No implicit widening. Both sides must be the same numeric type. `String + ` is valid: the right-hand side is coerced to `String` automatically. ```vertex "adult (age " + age + ")" // age is an Int. Coerced automatically ``` ### Comparison [Section titled “Comparison”](#comparison) `==`, `!=`, `<`, `<=`, `>`, `>=`. Work on all numeric types and on `Bool`/`String` for equality. ### Logical [Section titled “Logical”](#logical) `&&`, `||` (both short-circuit), `!` (unary). ### Unary [Section titled “Unary”](#unary) `-` (numeric negation), `!` (boolean negation).
---
# Option
> The Option type for representing optional values without null in Vertex.
`Option` is a built-in sum type with two variants: `Some` and `None`. ```vertex fn find_user( id: Int, ): Option { if id == 1 { Some("Alice") } else { None } } ``` `Some(value)` wraps a value. `None` is a unit variant. No call syntax. ```vertex match find_user(42) { Some(name) => "Hello, ${name}", None => "not found", } ``` ### `Option` fields in constructors [Section titled “Option\ fields in constructors”](#optiont-fields-in-constructors) Any field declared `Option` in a variant constructor is optional at the call site. Omitting it is equivalent to passing `None`: ```vertex type Contact { Contact(name: String, phone: Option) } let c = Contact(name: "Alice") // phone defaults to None let c2 = Contact(name: "Bob", phone: None) // same as above ``` Bare `None` is assignable to any `Option` field regardless of `T`. ### Combinator methods [Section titled “Combinator methods”](#combinator-methods) | Method | Signature | Returns | Description | | ------------------------------ | ----------- | ------------- | --------------------------------------------------------------------------------- | | `isSome()` | `Option` | `Bool` | `true` if this is `Some`. | | `isNone()` | `Option` | `Bool` | `true` if this is `None`. | | `unwrapOr(default: T)` | `Option` | `T` | Returns the inner value, or `default` if `None`. The default is always evaluated. | | `unwrapOrElse(f: fn(): T)` | `Option` | `T` | Returns the inner value, or calls `f()` if `None`. | | `or(other: Option)` | `Option` | `Option` | Returns the receiver if `Some`, otherwise `other`. | | `map(f: fn(T): U)` | `Option` | `Option` | Applies `f` to the inner value if `Some`. `None` passes through. | | `flatMap(f: fn(T): Option)` | `Option` | `Option` | Applies `f` if `Some` and returns the resulting `Option` directly. | | `toResult(error: E)` | `Option` | `Result` | `Some(v)` → `Ok(v)`, `None` → `Error(error)`. | ```vertex let name: Option = find_user(42) // unwrap with default let n: String = name.unwrapOr("guest") // transform if present let upper: Option = name.map(fn(s: String): String { s.toUpperCase() }) // pipeline: map then unwrap let label: String = name .map(fn(s: String): String { "Hello, " + s }) .unwrapOr("Hello, guest") // convert to Result let result: Result = name.toResult("not found") ```
---
# Output
> How to produce observable output from a Vertex program.
`debug` is the only way to produce observable output in Vertex. It is an **expression**: it evaluates its argument, prints it, and returns the value. It can appear anywhere an expression is valid. ```vertex debug "hello world" debug 42 debug factorial(10) ``` Because `debug` returns its argument, you can drop it into the middle of an expression to inspect an intermediate value without rearranging the code: ```vertex let total = debug compute_base() + compute_tax() // prints base, then adds tax ``` ## Stringification [Section titled “Stringification”](#stringification) `debug` converts its argument to a string before printing, following these rules: | Type | Representation | | -------------- | ---------------------------------------------------- | | `Int`, `Long` | Decimal digits, no leading zeros; negatives with `-` | | `Double` | Shortest decimal form that round-trips | | `Decimal` | Exact decimal form, scale preserved | | `Bool` | `true` or `false` | | `String` | The raw characters, no surrounding quotes | | `Option` | `Some(value)` or `None` | | `Result` | `Ok(value)` or `Error(value)` | | `List` | `[elem1, elem2, ...]` | | `Map` | `{k: v, ...}` (iteration order) | | Sum types | `Variant(field1, field2)` or just `Variant` for unit | ## Output streams [Section titled “Output streams”](#output-streams) In JIT mode, `debug` writes to **stdout**. Every other diagnostic the compiler or JIT produces (warnings, DML notices, test output headers) goes to **stderr**. A program that prints via `debug` and a runner that reads stdout see each other cleanly. In Salesforce mode, `debug` translates to Apex `System.debug(...)`. Output appears in the Apex debug log, not in your terminal. `vertex run --sf` filters the log and prints only the lines that correspond to Vertex’s `debug` calls. ## `W001`: the debug-in-source warning [Section titled “W001: the debug-in-source warning”](#w001-the-debug-in-source-warning) The checker emits a `W001` warning wherever `debug` appears in your code: ```plaintext warning[W001]: `debug` in source at billing.vtx:18:5 | 18 | debug "in compute_total" | ^^^^^ debug output is a dev tool; remove before shipping ``` `debug` is a development aid, not a logging API. `W001` is the reminder to remove it before committing. The warning is suppressed for scripts executed via `vertex run` (where `debug` is the point) but not in `vertex build`. Caution If you find yourself wanting a real log, go through Apex’s logging facilities via [Apex FFI](/reference/salesforce/apex-ffi/). `debug` is intentionally limited to developer iteration. ## Why not `print` or `println`? [Section titled “Why not print or println?”](#why-not-print-or-println) There is one way to produce output. No `print`, no `println`, no `System.out.*`. `debug` is the one way, and the `W001` warning means you see a reminder every time you leave one in.
---
# Pattern Matching
> Exhaustive match expressions, let assert, and pattern syntax in Vertex.
### `match`: exhaustive, checked by the compiler [Section titled “match: exhaustive, checked by the compiler”](#match-exhaustive-checked-by-the-compiler) ```vertex fn area( s: Shape, ): Double { match s { Circle(radius: r) => 3.14159 * r * r, Rectangle(width: w, height: h) => w * h, Triangle(base: b, height: h) => 0.5 * b * h, } } ``` ### Positional destructuring [Section titled “Positional destructuring”](#positional-destructuring) ```vertex match s { Circle(r) => 3.14159 * r * r, Rectangle(w, h) => w * h, } ``` ### Destructuring shorthand (field punning) [Section titled “Destructuring shorthand (field punning)”](#destructuring-shorthand-field-punning) When the binding variable has the same name as the field, a trailing colon (`field:`) can be used instead of repeating the name (`field: field`): ```vertex // Long form let Book(title: title, author: author, genre: genre) = b // Shorthand. Equivalent let Book(title:, author:, genre:) = b ``` Shorthand and explicit rename can be freely mixed: ```vertex let Book(title:, author: a, genre:) = b // title and genre use shorthand ``` The shorthand is valid in all pattern positions: `let`, `let assert`, `match` arms, and `for` loop heads. ### Discarding unmentioned fields in variant patterns [Section titled “Discarding unmentioned fields in variant patterns”](#discarding-unmentioned-fields-in-variant-patterns) A trailing `..` inside a variant or record pattern means “I don’t care about the remaining fields”. Without it, you must bind every field the type declares. ```vertex match b { Book(title:, ..) => debug title, // ignore author, genre, isbn, ... } ``` `..` is only valid at the end of the field list and binds nothing. If you want to *capture* something, use `...`; if you want to *discard*, use `..`. ### Match guards [Section titled “Match guards”](#match-guards) ```vertex match score { n if n >= 90 => "A", n if n >= 80 => "B", _ => "F", } ``` ### Wildcard `_` [Section titled “Wildcard \_”](#wildcard-_) Matches anything, binds nothing. ```vertex match weekday { Sat => true, Sun => true, _ => false, } ``` ### Literal patterns [Section titled “Literal patterns”](#literal-patterns) ```vertex match status { "active" => "go", "paused" => "wait", _ => "unknown", } ``` ### Multi-subject match [Section titled “Multi-subject match”](#multi-subject-match) ```vertex let msg = match isAdmin, isActive { true, true => "Active admin", true, false => "Inactive admin", false, _ => "User", } ``` ### OR-patterns [Section titled “OR-patterns”](#or-patterns) Multiple alternatives in a single arm. All alternatives must bind the same names with the same types. ```vertex match ticket { VIP(price: p) | Bleachers(price: p) | GeneralAdmission(price: p) => p, FreePass => 0, } ``` Literal alternatives work too: ```vertex match status { "active" | "pending" => true, _ => false, } ``` OR-patterns can have guards; the guard applies to the whole arm: ```vertex match ticket { VIP(price: p) | Bleachers(price: p) if p > 50 => "premium", _ => "standard", } ``` OR-patterns are only valid as the top-level arm pattern in a single-subject `match`. They are not valid in `let`, `let assert`, `for`, or multi-subject match arms. ### List patterns (inside `match` only) [Section titled “List patterns (inside match only)”](#list-patterns-inside-match-only) ```vertex match items { [] => "empty", [head, ...rest] => "starts with ${head}", [a, b, ..] => "at least two elements", } ``` **Rule of thumb:** `...` binds or spreads; `..` discards. Three dots does something with the rest; two dots throws it away. The same rule applies to list patterns (`[head, ...rest]` vs `[a, b, ..]`) and to variant patterns (`Book(title:, ..)`) , `...` has no form in variant patterns since variant fields are named, not positional. ### `let assert`: runtime-checked single-pattern extraction [Section titled “let assert: runtime-checked single-pattern extraction”](#let-assert-runtime-checked-single-pattern-extraction) Caution Panics at runtime if the pattern does not match. Use only when you know the variant is guaranteed. ```vertex let assert Ok(value: n) = someResult let assert Circle(radius: r) = shape ``` Valid patterns in `let assert`: variant, tuple, identifier, wildcard. List patterns and literal patterns are **not** valid in `let assert`.
---
# Primitive Types
> Vertex's built-in primitive types and their literal syntax.
| Type | Literal syntax | Example | | --------- | ----------------------------- | ---------- | | `Int` | plain integer | `42` | | `Long` | integer with `L` suffix | `100L` | | `Double` | decimal point, no suffix | `3.14` | | `Decimal` | decimal point with `d` suffix | `9.99d` | | `Bool` | `true` / `false` | `true` | | `String` | double-quoted | `"hello"` | | `Id` | no literal. Runtime only | (from DML) | | `Void` | the keyword `Void` | `Void` | `Void` is a real first-class value. A function that only produces side effects declares `: Void` and ends with `Void` as its trailing expression. ```vertex fn log( message: String, ): Void { debug message Void } ``` No implicit widening between numeric types. Both sides of an arithmetic expression must be the same type.
---
# Result
> The Result type for representing success or failure without exceptions in Vertex.
`Result` is a built-in sum type with `Ok` and `Error` variants. ```vertex fn safe_divide( a: Int, b: Int, ): Result { if b == 0 { Error("cannot divide by zero") } else { Ok(a / b) } } ``` ```vertex match safe_divide(10, 2) { Ok(v) => "result: " + v, Error(e) => "error: " + e, } ``` ### Combinator methods [Section titled “Combinator methods”](#combinator-methods) | Method | Signature | Returns | Description | | -------------------------------- | ------------- | ------------- | --------------------------------------------------------------------------------- | | `isOk()` | `Result` | `Bool` | `true` if this is `Ok`. | | `isError()` | `Result` | `Bool` | `true` if this is `Error`. | | `unwrapOr(default: T)` | `Result` | `T` | Returns the `Ok` value, or `default` if `Error`. The default is always evaluated. | | `unwrapOrElse(f: fn(E): T)` | `Result` | `T` | Returns the `Ok` value, or calls `f(error)` if `Error`. | | `map(f: fn(T): U)` | `Result` | `Result` | Applies `f` to the `Ok` value. `Error` passes through. | | `mapError(f: fn(E): F)` | `Result` | `Result` | Applies `f` to the `Error` value. `Ok` passes through. | | `flatMap(f: fn(T): Result)` | `Result` | `Result` | Applies `f` if `Ok` and returns the resulting `Result` directly. | | `toOption()` | `Result` | `Option` | `Ok(v)` → `Some(v)`, `Error(_)` → `None`. | ```vertex let r: Result = safe_divide(10, 2) // unwrap with default let n: Int = r.unwrapOr(0) // transform the Ok value let doubled: Result = r.map(fn(x: Int): Int { x * 2 }) // convert Error to a different type let reMapped: Result = r.mapError(fn(e: String): Int { e.length() }) // chain operations let final: Int = r .flatMap(fn(x: Int): Result { Ok(x + 1) }) .unwrapOr(0) // convert to Option (discards error) let opt: Option = r.toOption() ``` ### `?` operator: early error propagation [Section titled “? operator: early error propagation”](#-operator-early-error-propagation) Applied to a `Result` expression inside a function that also returns `Result<_, E>`. If the value is `Error(e)`, propagates immediately; otherwise unwraps the `Ok` value. ```vertex fn process( raw: Int, ): Result { let age = validate_age(raw)? Ok(label_age(age)) } ```
---
# Salesforce
> SObjects, DML, AuraEnabled, Apex FFI, and the Apex.Object escape hatch.
Vertex is a Salesforce-first language. This section is the reference for everything that touches the Apex runtime: SObject types, DML, Lightning components, and calling into existing Apex code. ## Reading order [Section titled “Reading order”](#reading-order) If you are new to the Salesforce side of Vertex, read these pages in order. Each one builds on the previous. 1. **[Salesforce Integration](/reference/salesforce/integration/)**: `@SObject` types, the `Database.*` DML API, and `@AuraEnabled` functions and types. Start here for day-to-day Salesforce work. 2. **[Apex FFI](/reference/salesforce/apex-ffi/)**: `extern type`, `extern new`, `extern method`, `extern field`, and `extern enum` for calling existing Apex code from Vertex. Read this when you need to reuse Apex libraries or wrap `System.*` APIs. 3. **[Apex.Object](/reference/salesforce/apex-object/)**: the escape hatch for Apex’s universal `Object` type. Read this when you are working with dynamic Apex APIs that return or accept `Object`. ## Related [Section titled “Related”](#related) Annotations `@Test`, `@AuraEnabled`, `@SObject`, and sharing modifiers. [Annotations reference](/reference/annotations/) Testing `@Test`, the simulated DML layer, and running tests locally before deploying. [Testing reference](/reference/testing/) Building for Salesforce Task-oriented guide to building a complete Salesforce project. [Guide: Building for Salesforce](/guides/salesforce-projects/)
---
# Apex FFI, Extern Declarations
> Referencing existing Apex types, methods, and fields from Vertex code.
Vertex can reference existing Apex types through `extern` declarations. Extern declarations are **compile-time metadata only**. They tell the type checker that a foreign type exists, and the codegen emits references to it verbatim. No Apex class is generated for an extern declaration. ## `extern type`: Opaque Apex class [Section titled “extern type: Opaque Apex class”](#extern-type-opaque-apex-class) ```vertex extern type Messaging.SingleEmailMessage extern type Schema.DescribeSObjectResult pub extern type Limits ``` `extern type` declares an Apex class as an opaque Vertex type. The type can be used in parameter and return positions and as generic type arguments, but it has no constructors until `extern new` is declared. ```vertex extern type Messaging.SingleEmailMessage fn wrapAll( msg: Messaging.SingleEmailMessage, ): List { [msg] } ``` Generates: ```apex private static List wrapAll(Messaging.SingleEmailMessage msg) { return new List{msg}; } ``` ## `extern fn`: Apex static method [Section titled “extern fn: Apex static method”](#extern-fn-apex-static-method) ```vertex extern fn Limits.getCpuTime(): Int extern fn Messaging.reserveSingleEmailCapacity(count: Int): Void ``` `extern fn` declares an Apex static method as a callable Vertex function. ```vertex extern fn Limits.getCpuTime(): Int fn logCpu(): Int { Limits.getCpuTime() } ``` Generates: ```apex private static Integer logCpu() { return Limits.getCpuTime(); } ``` ### `= "apexName"` override [Section titled “= "apexName" override”](#-apexname-override) When two Apex overloads share the same name but differ by arity, each can be declared as a distinct `extern fn` with a `= "..."` override: ```vertex extern fn Messaging.sendEmail( mails: List, ): Void extern fn Messaging.sendEmailChecked( mails: List, allOrNothing: Bool, ): Void = "Messaging.sendEmail" ``` Caution Extern fns are not callable in JIT (`vertex run`) mode. They require the Salesforce runtime. Calling an extern fn in JIT mode produces a runtime error. ## `extern new`: Apex constructor [Section titled “extern new: Apex constructor”](#extern-new-apex-constructor) ```vertex extern type Messaging.SingleEmailMessage extern new Messaging.SingleEmailMessage(): Messaging.SingleEmailMessage ``` Vertex code calls it using `TypePath.new()`: ```vertex let msg = Messaging.SingleEmailMessage.new() ``` Generates: `Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();` ### Named constructor overloads [Section titled “Named constructor overloads”](#named-constructor-overloads) ```vertex extern new Messaging.SingleEmailMessage(): Messaging.SingleEmailMessage extern new Messaging.SingleEmailMessage.withSubject( subject: String, ): Messaging.SingleEmailMessage ``` Call sites: ```vertex let plain = Messaging.SingleEmailMessage.new() let subj_msg = Messaging.SingleEmailMessage.withSubject("Hello") ``` Generates: ```apex new Messaging.SingleEmailMessage() new Messaging.SingleEmailMessage('Hello') ``` ## `extern method`: Apex instance method [Section titled “extern method: Apex instance method”](#extern-method-apex-instance-method) ```vertex extern method Messaging.SingleEmailMessage.setSubject( self: Messaging.SingleEmailMessage, subject: String, ): Void ``` The first parameter must be named `self` and carry the receiver type. It is excluded from the call-site signature. ```vertex let msg = Messaging.SingleEmailMessage.new() msg.setSubject("Hello") ``` When the Apex method name differs from the Vertex name, use a `= "..."` override: ```vertex extern method Messaging.InboundEmail.getFrom( self: Messaging.InboundEmail, ): String = "getFromAddress" ``` ## `extern field`: Read-only Apex field [Section titled “extern field: Read-only Apex field”](#extern-field-read-only-apex-field) ```vertex extern field Messaging.InboundEmail.subject: String ``` Vertex code can read the field via dot access; writes are rejected by the checker (error `E120`). ## `extern var`: Read-write Apex field [Section titled “extern var: Read-write Apex field”](#extern-var-read-write-apex-field) ```vertex extern var Messaging.SingleEmailMessage.optOutPolicy: String ``` Reads are always allowed. Writes require the receiver to be declared `mutable let`; writing through an immutable binding is error `E119`. ```vertex fn configurePolicy(msg: Messaging.SingleEmailMessage): Void { mutable let m = msg m.optOutPolicy = "SEND_OPT_OUT" // ok. Local binding is mutable } ``` Field assignment syntax: `receiver.field = value` emits `receiver.field = value;` verbatim in Apex. ## `extern enum`: Apex enum type [Section titled “extern enum: Apex enum type”](#extern-enum-apex-enum-type) ```vertex extern enum Schema.DisplayType { Address, Currency, Phone, Picklist, Reference } ``` Maps an Apex enum type into Vertex as a sum type with unit variants. ```vertex let dt: Schema.DisplayType = Address ``` Match arms use `==` comparison in Apex: ```vertex fn label(dt: Schema.DisplayType): String { match dt { Address => "mailing address", Phone => "phone number", Reference => "lookup to another object", _ => "other", } } ``` Generates: ```apex if (dt == Schema.DisplayType.Address) { return 'mailing address'; } else if (dt == Schema.DisplayType.Phone) { return 'phone number'; } else if (dt == Schema.DisplayType.Reference) { return 'lookup to another object'; } else { return 'other'; } ``` Extern enum variants can be constructed and matched in JIT mode without a Salesforce connection. ## FFI safety rules [Section titled “FFI safety rules”](#ffi-safety-rules) ### `Option` return: null-check wrapper [Section titled “Option\ return: null-check wrapper”](#optiont-return-null-check-wrapper) When an extern declaration returns `Option`, the codegen wraps every call site with a null check: ```vertex extern fn Limits.getHeapSize(): Option let heap = Limits.getHeapSize() // heap: Option ``` Generated Apex: ```apex Integer vtxFfiRaw0 = Limits.getHeapSize(); Option vtxFfiResult0 = vtxFfiRaw0 != null ? (Option) new Some(vtxFfiRaw0) : (Option) new None(); final Option heap = vtxFfiResult0; ``` The programmer is responsible for knowing whether the Apex API can return `null`. If declared non-`Option` and the Apex side returns `null`, a `NullPointerException` propagates in Apex. ### `Result` return: try/catch wrapper [Section titled “Result\ return: try/catch wrapper”](#resultt-apexexception-return-trycatch-wrapper) When an extern declaration returns `Result`, the codegen wraps every call site with a `try`/`catch`. ```vertex extern fn Database.countQuery(soql: String): Result let result = Database.countQuery("SELECT COUNT() FROM Account") match result { Ok(n) => debug "count: " + n.toString(), Error(e) => debug "error: " + e.message, } ``` The `ApexException` type carries: | Field | Type | Description | | ------------ | ----------------------- | ------------------------- | | `message` | `String` | Exception message | | `typeName` | `String` | Apex exception class name | | `stackTrace` | `String` | Stack trace string | | `cause` | `Option