Durable Execution
Compile selected programs into typed, resumable plans
Durable execution is opt-in. An Action defines a closed persistence boundary, and a Flow turns Action calls into a statically analyzable plan.
:::warning Design draft The Action and Flow model is accepted, but the runtime and compiler are not implemented. Provider spelling, stable IDs, loop semantics, wire encoding, and deployment APIs remain under design. :::
Define an Action
An Action closes over one function signature:
abstract class Compile extends Action<
(input: CompileInput) => Result<CompileOutput, CompileError>
> {}
abstract class Package extends Action<
(input: PackageInput) => Result<Artifact, PackageError>
> {}The compiler derives the input, success, and Error codecs from those types. The author does not repeat schemas in an Action constructor.
Action values crossing a persistence or machine boundary must satisfy Durable<T>. Plain data derives this automatically. Functions, process handles, and other ephemeral values require an explicit durable representation or are rejected.
Build a Flow
Import the compiler-recognized durable function and pass it a statically resolvable function:
import { durable } from "vibelang:flows"
const Build = durable(function Build(
input: BuildInput,
): Result<Artifact, CompileError | PackageError> {
const compiled = Compile.run({ source: input.source }).unwrap()
return Package.run({ code: compiled.code })
})durable is ordinary call syntax, not a keyword. The compiler recognizes its resolved import identity, so aliases work and an unrelated function named durable remains ordinary. An inline function, declaration, or function-valued constant is accepted when the compiler can resolve it statically; a runtime-selected callback is rejected.
The compiler lowers the function's checked syntax, control flow, and data flow. It does not invoke the function with proxy values. Compile.run becomes an Action node and a typed symbolic value. Accessing compiled.code creates a projection, and passing that value to Package.run creates a data edge. No Action implementation runs during compilation or planning.
The durable source function disappears after compilation. The emitted template contains the plan, codecs, inferred failures and requirements, stable identities, and debug map.
Compilation Phases
Durable programs have four distinct phases:
- Template compilation statically lowers the durable function into target-neutral Plan IR without invoking it.
- Deployment build resolves providers, checks the dependency closure, and partitions coordinator and worker artifacts.
- Plan/preview reads emitted Plan IR, validates and optionally specializes known input, and reports the graph without loading the durable function or dispatching Actions.
- Execution creates or resumes a durable run and dispatches eligible Action nodes from the Plan IR.
Keeping the phases separate allows one plan to deploy across TypeScript, native, and Wasm workers. If a branch or runtime-sized fan-out cannot be resolved from preview inputs, plan mode reports the conditional or parameterized template instead of executing source code to guess its shape.
Provide Implementations
Actions are abstract capabilities. Layers install ordinary function implementations and their policies:
import { Action, Layer } from "vibelang/provider"
const BuildActions = Layer.merge(
Action.provide(Compile, compileWithEsbuild, {
recovery: Action.idempotent,
reuse: Action.content,
}),
Action.provide(Package, packageArtifact, {
recovery: Action.idempotent,
reuse: Action.execution,
}),
)The abstract Action owns the durable contract. A deployment layer chooses its implementation, placement, resource limits, and recovery policy.
Control Flow in Plans
Branches on comptime values reduce normally. Branches on Flow input or Action output emit plan nodes and compile both arms:
import { durable } from "vibelang:flows"
const Release = durable(function Release(
input: ReleaseInput,
): Result<Deployment | FailureReport, TestError | DeployError | ReportError> {
const tested = Test.run({ source: input.source }).unwrap()
return if (tested.passed) {
Deploy.run({ artifact: tested.artifact })
} else {
ReportFailure.run({ report: tested.report })
}
})Pure operations on symbolic values become portable expression IR. An operation the IR cannot represent is a compile error; the compiler never runs the source against a fake placeholder.
Replay and Retry
Every completed Action invocation is journaled for its Flow execution. After a restart, the same execution observes the recorded success or Error Result instead of repeating completed work.
Retry safety is a separate decision:
- idempotent work may repeat after an ambiguous crash
- compensable work may repeat with an explicit rollback path
- irreversible work does not retry after ambiguity
The safe default is execution-local replay with no ambiguous retry unless the implementation declares how repetition is safe.
Memoization and Content Caching
Cross-execution reuse has two meanings:
- Memoization records one acceptable result for an explicit key, even when the operation is nondeterministic.
- Content caching asserts that complete keyed inputs, implementation, dependencies, and execution semantics reproduce an equivalent result.
These policies are never interchangeable. A memoized model response must not be promoted to a hermetic shared cache entry.
Distributed Builds
A deployment can emit:
- a coordinator artifact
- several tree-shaken worker artifacts
- derived RPC bindings and schemas
- a signed routing manifest
Different worker pools can use TypeScript, native, or Wasm implementations of the same Action. Placement belongs to the deployment layer, so the durable signature stays independent of one machine topology.
Security Boundary
The Action signature describes data, not authority. Worker implementations receive only their declared providers, and coordinator-to-worker messages are validated against derived codecs.
OS isolation, secret delivery, fencing, artifact storage, and worker attestation remain runtime responsibilities built around the compiler-derived contract.