# Native TypeScript Workflows
A3S Flow remains a Rust SDK. `NativeTsRuntime` is an optional runtime adapter
for hosts that want workflow authors to write TypeScript while Rust still owns
run creation, event storage, replay, workers, scheduling, and inspection.
This is not a TypeScript SDK. The TypeScript code is source for a native runtime
artifact that a Rust host compiles and invokes.
## Compiler Contract
`NativeTsRuntime` expects a compiler executable with this command shape:
```sh
a3s-flow-native-compiler compile <entrypoint.ts> -o <artifact>
```
The produced artifact must:
- be executable by the host,
- accept `--a3s-flow-runtime`,
- read one `NativeRuntimeRequest` JSON object from stdin,
- write one `NativeRuntimeResponse` JSON object to stdout,
- dispatch workflow requests to the `exportName` function from the request,
- dispatch step requests by `payload.step_name`.
Set a custom compiler path in Rust:
```rust
let runtime = NativeTsRuntime::new(NativeTsRuntimeConfig::new(
"/path/to/a3s-flow-native-compiler",
".a3s-flow/native-ts",
".",
));
```
The runnable example also accepts:
```sh
A3S_FLOW_NATIVE_TS_COMPILER=/path/to/a3s-flow-native-compiler \
cargo run --example native_ts_greeting
A3S_FLOW_NATIVE_TS_COMPILER=/path/to/a3s-flow-native-compiler \
cargo run --example native_ts_preflight
```
When that environment variable is not set, the examples print the missing
prerequisite and exits successfully so the normal Rust example suite remains
portable.
## Preflight And Diagnostics
Call `NativeTsRuntime::preflight(&spec)` before accepting user-authored source or
starting a run when a host wants early validation and compiler diagnostics.
```rust
let preflight = runtime.preflight(&spec).await?;
println!("entrypoint={}", preflight.entrypoint.display());
println!("artifact={}", preflight.artifact.display());
println!("source_hash={}", preflight.source_hash);
println!("cache_hit={}", preflight.cache_hit);
```
Preflight performs the same compile path used by workflow and step execution:
- validates that the `WorkflowSpec` is a valid `native_ts` spec,
- resolves the entrypoint relative to the runtime working directory,
- hashes the source and runtime identity fields into the artifact cache key,
- compiles the source only when the artifact cache is cold,
- returns `NativeTsRuntimePreflight` with entrypoint, artifact, source hash, and
cache-hit metadata,
- returns a runtime error containing compiler stderr when compilation fails.
Use `examples/native_ts_preflight.rs` when testing compiler installation,
artifact cache paths, or CI diagnostics without starting a workflow run.
## Protocol Envelope
The request envelope is stable and versioned:
```json
{
"protocol": "a3s.flow.native_ts.v1",
"kind": "workflow",
"exportName": "main",
"sourceHash": "sha256...",
"payload": {
"run_id": "run-id",
"input": {},
"history": []
}
}
```
The response envelope must mirror the request kind:
```json
{
"protocol": "a3s.flow.native_ts.v1",
"kind": "workflow",
"ok": true,
"output": {
"type": "complete",
"output": {}
}
}
```
For step requests, `output` is the step output JSON value. For workflow
requests, `output` is a `RuntimeCommand`.
## Authoring Types
Use [`examples/native-ts/a3s-flow-runtime.d.ts`](../examples/native-ts/a3s-flow-runtime.d.ts)
as the authoring contract for workflow and step source. It mirrors the Rust
serde field names used in `NativeRuntimeRequest`, `WorkflowInvocation`,
`StepInvocation`, `RuntimeCommand`, and `FlowEventEnvelope`. The file is a type
contract, not a runtime helper module, so workflow source should define local
history helpers or rely on helpers injected by the compiler artifact.
The contract defines:
- `WorkflowInvocation<Input>`
- `StepInvocation<Input>`
- `RuntimeCommand`
- `RetryPolicy`
- `FlowEvent`
- `FlowEventEnvelope`
- `StepDefinition<Input, Output>`
- `NativeRuntimeRequest<Payload>`
- `NativeRuntimeResponse<Output>`
Important protocol details:
- `FlowEventEnvelope` includes `event_id`, `run_id`, `sequence`, `timestamp`,
and `event`. It does not include a derived event key.
- `create_hook` commands and `hook_created` history events include a required
`token` because callback routing must be stable across replay.
- `step_retrying.retry_after` is `string | null`, matching Rust's serialized
`Option<DateTime<Utc>>`.
- `schedule_step.retry` and batched `StepCommand.retry` may be omitted; Rust
applies the default retry policy.
The greeting source in
[`examples/native-ts/greeting.ts`](../examples/native-ts/greeting.ts) shows the
intended shape:
```ts
import type {
RuntimeCommand,
StepInvocation,
WorkflowInvocation,
} from "./a3s-flow-runtime";
export async function main(
invocation: WorkflowInvocation<GreetingInput>,
): Promise<RuntimeCommand> {
// Inspect invocation.history and return the next deterministic command.
}
export const steps = {
async greet_user(invocation: StepInvocation<GreetingStepInput>) {
// Do side effects here and return persisted JSON output.
},
};
```
## Determinism Rules
Workflow exports should be deterministic:
- read only `input` and `history`,
- return exactly one `RuntimeCommand`,
- do not perform network, clock, random, filesystem, or shell work,
- put side effects in step handlers,
- use stable step IDs, wait IDs, and hook IDs.
- set `retry.on_exhausted` to `"continue_workflow"` only when workflow replay
explicitly handles the resulting `step_failed` history.
Step handlers may perform side effects, but their outputs are persisted before
workflow replay observes them.