If it compiles, it runs
Static types, exhaustive match, Option<T> in place of null,
and Result<T, E> in place of exceptions. The compiler catches a
lot of what ships as a production bug in Apex.
Here is a taste:
fn validate_age(age: Int): Result<Int, String> { 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 25Sum 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.
If it compiles, it runs
Static types, exhaustive match, Option<T> in place of null,
and Result<T, E> 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.
No more null pointers. Absence is Option<T> and you have to handle
both cases to get at the value:
type User { User(name: String) }
fn greet(user: Option<User>): 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<T, E>, and
? propagates the error when you want the happy path to read cleanly:
fn check_positive(n: Int): Result<Int, String> { if n < 0 { Error("negative: ${n}") } else { Ok(n) }}
fn process(raw: Int): Result<String, String> { 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:
$ vertex run hello.vtxHello, Alice!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 to install and run your first program, or dig into the Language Tour. If you prefer reading a real codebase, there is a sample library management app at vertex-run/vertex-library-management.