This page is the per-step reference: every WorkflowStepDef variant and every Expr kind the runtime executes, with the visible contract. For the formal grammar (EBNF, lexical rules, validation catalog) see Grammar. For the conceptual model — why the language is shaped this way — see Why Jaiph.
The runtime is NodeWorkflowRuntime (src/runtime/kernel/node-workflow-runtime.ts). Step dispatch is driven by WorkflowStepDef.type (8 variants). Value evaluation goes through one private evaluateExpr over Expr.kind (8 variants); see Architecture — AST / Types.
| Type | Operations | Crossings |
|---|---|---|
string |
${…} interpolation, run / ensure arguments, const, prompt body, send payload, return. |
Cannot be invoked with run (E_VALIDATE: strings are not executable). |
script |
Invocable with run. |
Not interpolatable, not const-assignable by name, not a valid prompt body. |
Crossings produce specific E_VALIDATE messages identifying the violated rule.
| Top-level | Description |
|---|---|
import "path" as alias |
Loads another module. .jh appended automatically. Resolution: relative-first, then library fallback (<workspace>/.jaiph/libs/<name>/...). |
import script "path" as name |
Loads an external script file (no .jh appended). Path is relative-only. Treated as a script symbol. |
export rule / export workflow / export script |
Marks a definition public. At least one export makes module visibility explicit; otherwise all top-level definitions are implicitly public. |
channel name [-> target [, target …]] |
Declares a named queue. Inline routes target workflows with exactly three parameters (message, channel, sender). |
const NAME = value |
Module-scoped immutable string. Values: double-quoted, triple-quoted, or bare token. Stored verbatim. |
config { … } |
Module-level configuration block (agent.*, run.*, runtime.*, module.*). See Configuration. |
rule name([params]) { … } |
Validation rule. Invoked with ensure. |
script name = … |
Executable definition. Invoked with run. |
workflow name([params]) { … } |
Orchestration entrypoint. Invoked with run (or by jaiph run for default). |
Visibility rule: when a module has at least one export, only exported names are reachable through its alias (E_VALIDATE: "<name>" is not exported from module "<alias>").
The unified per-module namespace covers channels, rules, workflows, scripts, script-import aliases, and top-level const. Duplicates are E_PARSE.
There are eight WorkflowStepDef variants. Every body line that does not match a managed form becomes a shell step (workflows only — rules reject unrecognised shell).
| Type | Surface | Description |
|---|---|---|
exec |
run / ensure / prompt / standalone match / inline shell |
Side-effecting managed call statement. The discriminator (call / inline_script / prompt / match / shell) lives in body.kind. Carries optional captureName, catch, or recover. |
const |
const NAME = <expr> |
Bind a value expression to a name. |
return |
return <expr> |
Set the managed return value. |
send |
channel <- <expr> |
Enqueue a payload on a channel for the current workflow context. |
say |
log / logerr / fail |
level: "log" / "logerr" / "fail". level: "fail" aborts with the message. |
if |
if <subject> <op> <operand> { … } [ else { … } ] |
Conditional block. |
for_lines |
for <iter> in <source> { … } |
Iterate lines of a string variable. |
trivia |
comments, blank lines | Formatter-only. Skipped by the runtime and validator. |
Expr kindsEvery value position (const RHS, return, send RHS, log / logerr / fail argument, and exec body) carries an Expr of one of eight kinds.
| Kind | Source form | Runtime behaviour |
|---|---|---|
literal |
"…", """…""", ${var}, post-dedent triple-quoted body |
Interpolated against the current scope; ${run …} / ${ensure …} perform inline managed calls. |
call |
run ref(args), run async ref(args) |
Managed workflow/script call. async: true on the run async capture position. |
ensure_call |
ensure ref(args) |
Managed rule call. |
inline_script |
`body`(args) / ```lang…body…``(args)` |
Inline script body emitted as scripts/__inline_<hash>. |
prompt |
prompt body [returns "<schema>"] |
Sends body to the agent backend; JSON-quoted in transport. |
match |
match <subject> { … } |
Walks arms top-to-bottom; first match wins. |
shell |
Free-form workflow body line; send RHS parse fallback | Workflow: unparsed line becomes an inline-shell exec step (rules forbid). Send: non-literal RHS fallback — usually E_VALIDATE. |
bare_ref |
A bare symbol on a send RHS |
Always rejected by the validator; preserved so the error can name the symbol. |
run — execute a workflow or script| Position | Allowed target |
|---|---|
run in workflow |
Workflow or named script. |
run in rule |
Named script only. Workflows / rules are E_VALIDATE. |
run async |
Workflows only. Inline scripts not supported. |
Inline-script run |
Allowed in both workflows and rules. |
Capture rules:
| Callee | Captured value |
|---|---|
| Workflow | Explicit return value of the callee. |
| Named script | Trimmed stdout. |
| Inline script | Trimmed stdout. |
Rule (ensure) |
Explicit return value. |
Inline scripts embed a script body in a step without a separate script definition. Single backticks for one-liners, triple backticks for multiline or polyglot bodies.
run `echo hello`()
const x = run `echo captured`()
const y = run `date +%s`()
run `echo $1-$2`("hello", "world") # => hello-world
| Aspect | Rule |
|---|---|
| Backtick form | ${…} Jaiph interpolation is E_PARSE. Use $1, $2, … |
| Fenced form | ${…} passes through to the shell. Optional lang tag selects the interpreter (```python3 `` → #!/usr/bin/env python3). |
| Mixing fence tag + manual shebang | Error. |
| Default shebang | #!/usr/bin/env bash when neither tag nor #! line is present. |
| Emitted name | scripts/__inline_<12-hex>; deterministic across runs. |
catch / recover |
Allowed on a standalone run step with inline-script body. Forbidden on inline scripts in log / logerr / return / const RHS. |
| Subprocess env | Same scope.env as named scripts (runner process.env plus Jaiph metadata). Module const values are not auto-exported — pass via $1, $2. |
run async |
Not supported. |
run async — concurrent execution with handlesrun async ref(args) starts the callee concurrently and returns a Handle<T> immediately. T is the same type a synchronous run would return.
workflow default() {
run async lib.task_a()
const h = run async lib.task_b()
log "${h}" # forces resolution of h (blocks until task_b finishes)
}
| Aspect | Behaviour |
|---|---|
| Resolution trigger | First non-passthrough read — string interpolation, argument to run / ensure, comparison in if / match, prompt body referencing ${h}, channel send payload referencing ${h}, or const copy = h (bare-identifier RHS desugars to "${h}"). |
| Passthrough | Initial capture (const h = run async foo()), bare run async with no capture name. |
| Implicit join | When the enclosing executeSteps scope exits, all remaining unresolved handles created there are joined. Failures aggregate like a synchronous step. |
recover / catch |
Both work with run async. recover uses the same retry-limit semantics as non-async recover (run.recover_limit). |
| Inline scripts | Not supported with run async. |
| Rule scope | run async in a rule is E_VALIDATE. |
| Progress display | Each branch is prefixed with subscript digits (₁, ₂, …) at the call site’s indent level, in dispatch order. Nested branches get their own numbering scope. |
See Spec — Async Handles for the full value model.
ensure — execute a ruleensure check_deps()
const result = ensure lib.validate(input)
ensure ci_passes() catch (failure) {
log "ci failed: ${failure}"
}
Succeeds when the rule’s exit code is 0. The capture binds the rule’s explicit return value. ensure does not accept recover — only catch.
catch and recoverBoth attach to run (any form) or to ensure (catch only). The binding receives the merged stdout+stderr from the failed execution.
| Form | Loop | Allowed on |
|---|---|---|
catch (name) <body> |
Runs the recovery body once on failure. | ensure and run (sync and async). |
recover (name) <body> |
Retries the target after each repair body until success or run.recover_limit (default 10). |
run only (sync and async). |
run deploy() catch (err) run rollback()
run deploy(env) recover(err) {
log "deploy failed: ${err}"
run auto_repair(env)
}
Validation rules:
| Rule | Behaviour |
|---|---|
| Binding required | Exactly one binding. Bare catch / recover is E_PARSE. |
| Argument placement | All call arguments inside () before catch / recover. |
| Mutual exclusion | A single run step accepts catch or recover but not both. |
| Inline-script position | catch / recover only on standalone run steps. Forbidden on inline scripts in log / logerr / return / const RHS. |
prompt — agent interactionSends text to the configured agent backend. Three body forms:
| Body form | Syntax |
|---|---|
| Single-line literal | prompt "Review the code" |
| Identifier | prompt my_text (my_text must be in scope) |
| Triple-quoted | prompt """\nMultiline body with ${vars}\n""" |
| Aspect | Rule |
|---|---|
| Capture | const name = prompt …. name = prompt … is E_PARSE. |
Typed returns |
Flat { field: type, … } with string / number / boolean. Stored verbatim as text per-field. |
Capture required when returns |
prompt … returns "…" without const is E_PARSE. |
| Dot notation | ${result.field} requires that the base is a typed-prompt capture and the field appears in the schema. |
| Rule scope | Forbidden — prompt and const … = prompt are E_VALIDATE inside rules. |
| Transport retry | Transport failures retry on a backoff schedule; deterministic post-processing failures do not. See Configuration — Prompt retry on transport failure. |
const — bind a valueconst tag = "v1.0"
const message = """
Hello ${name}
"""
const result = run helper(arg)
const check = ensure validator(input)
const answer = prompt "Summarize"
const label = match status {
"ok" => "success"
_ => "failure"
}
| RHS form | Notes |
|---|---|
| Double-quoted string | Single-line. Multi-line double-quoted is E_PARSE. |
| Triple-quoted block | Multiline; supports ${…}. |
run call / run async call / ensure call |
Managed capture. |
prompt (any body form) |
Optional returns schema. |
match expression |
Walks arms; first match wins. |
Bare ref(args) |
E_PARSE — wrap with run / ensure / prompt. |
$(…), ${var:-fallback}, etc. |
E_PARSE in const RHS. |
All bindings — parameters, const, captures, script names — are immutable in their scope. The validator names the conflicting binding and its origin (E_VALIDATE: cannot rebind immutable name "x"; already bound as parameter at file.jh:1).
return — managed return valuereturn "success"
return "${result}"
return response # sugar for return "${response}"
return run helper()
return ensure check(input)
return match status { "ok" => "pass", _ => "fail" }
return run `cat report.txt`()
| Form | Notes |
|---|---|
| String / triple-quoted | Verbatim with interpolation. |
| Bare identifier | Sugar for return "${ident}". Unknown identifier is E_VALIDATE. |
return run ref() / return ensure ref() |
Managed direct return. Requires (). return run helper without parens becomes a shell step. |
return run \…`(args)` |
Inline-script direct return. The run keyword is required. |
return match … { … } |
Match expression as the return value. return inside an arm body is forbidden. |
| Position | Only in rule and workflow bodies. Script bodies use echo/printf; bare return 0 / return $? in a script are shell exit codes. |
send — channel messagealerts <- "Build started"
reports <- ${output}
results <- run build_message(data)
alerts <- """
Build report for ${project}
"""
| Rule | Behaviour |
|---|---|
| RHS required | Bare channel <- is E_PARSE. |
| Allowed RHS | Double-quoted string, triple-quoted block, ${ident} / ${…}, run ref(args) (with parens). |
| Bare ref RHS | A bare workflow / rule / script name is E_VALIDATE. |
| Combined capture | name = channel <- … is E_VALIDATE (invalid send: channel must be a single name or …). |
| Allowed in | Workflows only. Rules forbid send. |
| Dispatch | send enqueues on the active workflow context. After that workflow’s steps complete successfully, the runtime drains the queue sequentially and runs each route target. Sends from nested workflows bubble to the nearest ancestor context that declares routes for the channel. See Inbox & Dispatch. |
log / logerr / faillog "Processing ${message}"
logerr "Warning: ${name} not found"
log status # bare identifier — same as log "${status}"
log run `date +%s`() # inline-script form (run keyword required)
log """
Build started at ${timestamp}
"""
fail "Missing configuration"
| Statement | Effect |
|---|---|
log |
Writes to the run’s stdout stream. Double-quoted messages store backslash sequences literally; use triple-quoted log """…""" for multiline text. |
logerr |
Writes to stderr. Displayed with ! marker in the progress tree. |
fail |
Aborts the workflow or rule with a stderr message and non-zero exit. |
Bare inline scripts in log / logerr (log \…`()) are E_PARSE — use log run `…`(args)`.
if — conditional guardif status == "ok" {
log "healthy"
} else {
logerr "unhealthy: ${status}"
}
if message =~ /ERROR/ {
logerr "matched error pattern"
}
| Aspect | Rule |
|---|---|
| Subject | Bare identifier or IDENT.IDENT (typed-prompt field access). |
| Operators | ==, != with double-quoted strings; =~, !~ with /regex/. Mixing kinds is E_PARSE. |
else |
Optional. } else { must be on a single line. else if chaining is not supported — nest if inside the else block or use match. |
| Value production | if is a statement. For value branching use match. |
| Async handles | Resolved before the comparison. |
| Allowed in | Workflows and rules. |
match — pattern matchmatch status {
"ok" => "all good"
/err/ => "something went wrong"
_ => "unknown"
}
| Aspect | Rule |
|---|---|
| Subject | Bare identifier or IDENT.IDENT. $var / ${var} is E_PARSE. |
| Patterns | String literal (exact equality), /regex/, or _ (wildcard — exactly one required). |
| Arm delimiter | Newlines. Commas between arms are E_PARSE. |
| Arm bodies | String literal, triple-quoted block, bare in-scope identifier, $var / ${var}, fail "…", run ref(…), ensure ref(…). |
| Disallowed in arms | return (use return match … { … } outside), inline scripts, unknown bare identifiers (E_VALIDATE: unknown identifier "…" in match arm body; declare it with "const", use a capture, or add a parameter). |
| Expression form | Usable with const x = match … or return match …. |
When a const x = match … step contains arms with run / ensure, the progress tree surfaces the called targets as child steps of the const row.
for — iterate lines of a stringconst paths = """
docs/a.md
docs/b.md
"""
for path in paths {
log "${path}"
}
| Aspect | Rule |
|---|---|
| Source variable | Must already hold a string (const, capture, parameter). Unknown name is E_VALIDATE. |
| Line splitting | Splits on \n (normalises \r\n). A trailing newline does not yield an empty final line. Interior empty lines are yielded. |
| Iterator name | Subject to the immutable-binding rules of the surrounding scope. After the loop, the iterator remains bound to the last line. |
| Allowed in | Workflows and rules. |
| Form | Status | Where |
|---|---|---|
${ident} |
Primary | All orchestration strings. |
${var.field} |
Typed-prompt field access | All orchestration strings. |
${run ref(args)} |
Inline capture — executes and inlines stdout / return value. | All orchestration strings. |
${ensure ref(args)} |
Inline capture — executes a rule and inlines result. | All orchestration strings. |
$ident (no braces) |
E_PARSE in orchestration strings. |
— |
$1, $2, … |
Positional args | script bodies only (interpretation depends on the interpreter). |
${var:-fallback}, ${var%%…}, ${var//…}, ${#var} |
E_PARSE in orchestration strings and backtick scripts; passes through in fenced scripts. |
— |
$(…) |
E_PARSE in orchestration strings. |
— |
If an inline capture fails, the enclosing step fails. Nested inline captures (${run foo(${run bar()})}) are E_PARSE — extract the inner call to a const.
Rules accept the same step set as workflows except:
| Step / form | Rule scope |
|---|---|
prompt |
Forbidden. |
const … = prompt … |
Forbidden. |
send (<-) |
Forbidden. |
run async |
Forbidden. |
run to a workflow |
Forbidden (run in rules targets scripts only). |
| Raw shell lines | Forbidden (every line must be a recognised Jaiph step). |
catch / recover on run |
Allowed. |
for, if, match |
Allowed. |
Compile-time enforcement: validate-step.ts consults RULE_SCOPE.allowSteps.
Managed script steps (run to a named script, import script, inline scripts) and workflow inline-shell lines all use the same scope.env: the runner’s process.env augmented by Jaiph (JAIPH_WORKSPACE, JAIPH_SCRIPTS, JAIPH_RUN_DIR, JAIPH_ARTIFACTS_DIR, JAIPH_RUN_ID, JAIPH_RUN_SUMMARY_FILE, prompt-related JAIPH_AGENT_*, and keys derived from config { … }). This is not an env -i-style wipe — anything the runner sees, the child sees, unless explicitly stripped.
Module const values are not automatically exported into script environments. Pass them as positional arguments ($1, $2, …) or read Jaiph-provided variables.
| Step | Status | Capture value | Logs |
|---|---|---|---|
ensure rule |
rule exit code | explicit return value |
rule artifacts |
run workflow |
workflow exit code | explicit return value |
workflow artifacts |
run script (named) |
script exit code | trimmed stdout | script .out / .err |
run `…`() (inline) |
script exit code | trimmed stdout | script .out / .err |
prompt |
prompt exit code | final assistant answer | transcript artifacts |
log / logerr |
always 0 | empty | event stream + stdout/stderr |
fail |
non-zero (abort) | empty | stderr |
run async |
aggregated | Handle<T> resolving on read |
async step artifacts |
const |
same as RHS step | empty (binds local) | n/a |
The runtime enforces a hard recursion depth limit of 256 (MAX_RECURSION_DEPTH in src/runtime/kernel/runtime-arg-parser.ts). Exceeding the limit produces a runtime error. The depth is the active workflow / rule call chain (not script subprocesses). There is no environment variable override.
send queueing and route execution semantics.