When you need the same workflow sources to behave differently on different machines, you separate what the graph does (rules, prompt / script / run, channels) from operational knobs: which LLM backend to use, where to write run logs and debug output, and how the CLI chooses host vs. Docker. Jaiph keeps the language stable and pushes those choices into configuration — in-file config blocks, environment variables, and defaults in the tool. Inbox dispatch order is defined by the language (sequential drain of route targets — see Inbox & Dispatch); it is not a configuration toggle.
All execution is interpreted by the Node workflow runtime (NodeWorkflowRuntime): the AST, managed scripts, prompts, channels, inbox, and .jaiph/runs artifacts (see Architecture). Configuration only adjusts that stack; it does not change the workflow language or the compile graph.
jaiph compile parses each module in the import closure (same grammar as emitScriptsForModule), so unknown config keys and wrong value types surface as the same parse diagnostics as before jaiph run. With a directory argument it treats every non-test *.jh file in that directory as its own entrypoint (see walkjhFiles — *.test.jh is skipped unless you pass a test file explicitly) and validates each entry’s transitive imports. validateReferences only — no scripts/ emission, no buildRuntimeGraph(), no runner spawn (see Architecture). Runtime graph loading is parse-only; compile-time reference validation runs in the transpile path, not in buildRuntimeGraph().
Source of truth: When this document and the implementation disagree, treat the source code as authoritative.
Jaiph provides three configuration mechanisms. When the same key is set in more than one place, the highest-priority source wins:
JAIPH_AGENT_*, JAIPH_RUNS_DIR, JAIPH_DEBUG, JAIPH_DOCKER_ENABLED, other JAIPH_DOCKER_*, and JAIPH_UNSAFE (for Docker on/off, see Sandboxing — Enabling Docker). Docker enablement is only controlled here — there is no runtime.* in-file key for that (removed; using it is a parse error with a migration message).config { ... } blocks — at module scope and optionally inside a workflow body.For agent and run keys, the full precedence chain is:
environment > workflow-level config > module-level config > defaults
run.recover_limit is an exception: only module-level values affect run … recover (see Run keys).
For runtime.* (image, network, timeout), the host CLI merges them when it may spawn Docker (resolveDockerConfig in src/runtime/docker.ts) — not inside NodeWorkflowRuntime. Precedence is JAIPH_DOCKER_* environment > module-level runtime.* > defaults (Docker on/off remains env-only, see above and Precedence in detail). A host invocation of jaiph run --raw skips that driver entirely and always runs the workflow runner locally (no container); runtime.* is unused on that path. Sandboxed workflows still run jaiph run --raw … inside the container. runtime.* cannot appear in workflow-level config blocks.
Each *.jh file may have at most one module-level config { ... } block. It is optional. Settings apply to all workflows in that file, unless a workflow has its own block.
jaiph run: the CLI reads only the entry file’s module config when it builds the initial process environment via resolveRuntimeEnv (before spawning the workflow runner or Docker). Imported modules’ module-level config is not merged into that first env snapshot — but the runtime still applies per-module and workflow config from the import graph when you enter a workflow, run a nested run in the same module, or ensure a rule (see Scoping across nested calls). Cross-module run and same-module ensure are special cases, explained there.
config {
agent.default_model = "gpt-4"
agent.backend = "claude"
agent.claude_flags = "--model sonnet-4"
run.logs_dir = ".jaiph/runs"
run.debug = false
}
script noop = `true`
rule some_rule() {
run noop()
}
workflow default() {
ensure some_rule()
}
Syntax rules:
config and { with only optional whitespace between them (and nothing else on that line before {).E_PARSE: duplicate config block (only one allowed per file).E_PARSE and list the allowed keys. Wrong value types also cause E_PARSE.A config { ... } block inside a workflow { ... } body overrides module-level agent and run keys for that workflow only. This is useful when different workflows in the same file need different models or backends.
config {
agent.backend = "cursor"
agent.default_model = "gpt-3.5"
}
script noop = `true`
rule some_rule() {
run noop()
}
workflow fast_check() {
config {
agent.backend = "claude"
agent.default_model = "gpt-4"
}
ensure some_rule()
}
workflow default() {
# Uses module-level config (cursor / gpt-3.5).
ensure some_rule()
}
Rules:
E_PARSE: duplicate config block inside workflow (only one allowed per workflow).agent.* and run.* keys are allowed. Any runtime.* or module.* key is E_PARSE.ensured rules and scripts called from it, for agent.* and run.logs_dir / run.debug (merged when the workflow or cross-module ensure runs). run.recover_limit is different: the retry limit for run … recover comes only from the module-level config of the .jh file that owns the current scope when the step runs; a workflow-level run.recover_limit assignment is valid syntax but does not change recover behavior today.Sibling isolation: Each workflow gets its own clone of the parent environment. Sibling workflows never see each other’s config — even when they execute sequentially. If workflow alpha sets agent.backend = "claude" and workflow beta only sets agent.default_model = "beta-model", beta still sees the module-level backend (e.g. "cursor"), not alpha’s.
| Type | Format | Example |
|——|——–|———|
| String | Double-quoted | "gpt-4" |
| Boolean | Unquoted true / false | true |
| Integer | Unsigned decimal digits only | 300 |
Recognized escapes inside strings: \\, \n, \t, \".
These control how prompt steps reach the LLM.
| Key | Type | Default | Env variable | Description |
|---|---|---|---|---|
agent.default_model |
string | (unset) | JAIPH_AGENT_MODEL |
Default model for prompt steps. |
agent.command |
string | cursor-agent |
JAIPH_AGENT_COMMAND |
Command line for the cursor backend. First token is the executable; the rest are leading arguments. When the command is not cursor-agent, Jaiph treats it as a custom agent command — prompt text is piped via stdin and raw stdout is captured. |
agent.backend |
string | cursor |
JAIPH_AGENT_BACKEND |
"cursor", "claude", or "codex". See Backend selection. |
agent.trusted_workspace |
string | workspace root | JAIPH_AGENT_TRUSTED_WORKSPACE |
Directory passed to Cursor (--trust). Relative paths are resolved against the workspace root at CLI launch. |
agent.cursor_flags |
string | (unset) | JAIPH_AGENT_CURSOR_FLAGS |
Extra flags appended for the cursor backend (split on whitespace). |
agent.claude_flags |
string | (unset) | JAIPH_AGENT_CLAUDE_FLAGS |
Extra flags appended for the claude backend (split on whitespace). |
These control runtime behavior unrelated to the agent.
| Key | Type | Default | Env variable | Description |
|---|---|---|---|---|
run.logs_dir |
string | .jaiph/runs |
JAIPH_RUNS_DIR |
Step log directory. Relative paths are joined with the workspace root; absolute paths are used as-is. |
run.debug |
boolean | false |
JAIPH_DEBUG |
Enables debug tracing for the run. |
run.recover_limit |
integer | 10 |
(no env override) | Maximum attempts for run … recover loops before the step fails (see Language — recover). Effective value comes only from the module-level config block of the .jh file that owns the current scope (the file containing the workflow or rule that executes the step). Workflow-level run.recover_limit does not apply. |
Optional descriptive metadata about the workflow module. These are informational only — they do not affect agent, run, or runtime behavior. Future features (e.g. MCP tool metadata) may consume them.
| Key | Type | Default | Description |
|---|---|---|---|
module.name |
string | (unset) | Human-readable name for this module. |
module.version |
string | (unset) | Version string (no validation — any quoted string is accepted). |
module.description |
string | (unset) | Short description of what this module does. |
Module keys can only appear in module-level config blocks. Any module.* key inside a workflow-level config is E_PARSE.
config {
module.name = "deploy-pipeline"
module.version = "2.0.0"
module.description = "Production deployment with rollback"
agent.backend = "claude"
}
workflow default() {
log "deploying..."
}
These configure Docker sandboxing. Unlike agent and run keys, they are read when the CLI considers a Docker launch for interactive jaiph run (src/cli/commands/run.ts → spawnExec). They never affect NodeWorkflowRuntime directly. They can only appear in module-level config blocks (not workflow-level).
Docker sandboxing is in beta. See Sandboxing for mounts, workspace layout, env forwarding, path remapping, and container behavior.
Host
--raw: If you runjaiph run --rawyourself on the host, the CLI does not enter the Docker branch; image/network/timeout merge is irrelevant for that invocation. Embedding and container flows use--rawinside the sandbox where the CLI has already picked the image — see Architecture.
| Key | Type | Default | Env variable | Description |
|---|---|---|---|---|
runtime.docker_image |
string | ghcr.io/jaiphlang/jaiph-runtime:<version> |
JAIPH_DOCKER_IMAGE |
Image name. Must already contain jaiph. When unset, uses the official GHCR image tag matching the installed jaiph version. For a custom image, build and push (or tag locally), then set this key or JAIPH_DOCKER_IMAGE. |
runtime.docker_network |
string | default |
JAIPH_DOCKER_NETWORK |
Docker network mode. |
runtime.docker_timeout_seconds |
integer | 3600 |
JAIPH_DOCKER_TIMEOUT |
Timeout in seconds (default one hour). Use 0 to disable. An invalid or negative environment value aborts the run with E_DOCKER_TIMEOUT (no silent fallback). In-file must be a non-negative integer. |
For agent and run keys, resolution order (highest wins):
JAIPH_AGENT_*, JAIPH_RUNS_DIR, JAIPH_DEBUG. When set, these lock the value for the entire process (see Locked variables).config — overrides module values for the duration of that workflow.config — applies to workflows that don’t define their own block.For Docker enablement on interactive jaiph run (no --raw on the host), the CLI uses JAIPH_DOCKER_ENABLED env > unsafe default rule (env only; runtime.docker_enabled is no longer supported). The default rule enables Docker unless JAIPH_UNSAFE=true is set; CI=true no longer disables Docker (see Sandboxing — Enabling Docker). Host jaiph run --raw never consults this branch. For other runtime.* keys (image, network, timeout), the merge is JAIPH_DOCKER_* env > module-level runtime.* > defaults whenever Docker launch is considered. Workflow-level config cannot set runtime keys.
When jaiph run builds the runner environment, any of these environment variables already present in process.env gets a matching ${NAME}_LOCKED flag set to "1":
JAIPH_AGENT_MODEL, JAIPH_AGENT_COMMAND, JAIPH_AGENT_BACKEND, JAIPH_AGENT_TRUSTED_WORKSPACE, JAIPH_AGENT_CURSOR_FLAGS, JAIPH_AGENT_CLAUDE_FLAGS, JAIPH_RUNS_DIR, JAIPH_DEBUG
Locked values cannot be overridden by module-level or workflow-level config — they are authoritative for the entire process. This is how environment variables always win in the precedence chain.
When workflows call into other workflows, the config scope depends on the call type:
| Call type | What happens |
|---|---|
Root entry (jaiph run file.jh) |
Full module + workflow metadata is applied (normal precedence). |
Same-module run |
Callee’s workflow-level config is layered on top of the caller’s effective env. Module-level config is not re-applied. |
Cross-module run (e.g. run alias.default) |
Caller’s effective env carries as-is. Callee’s module and workflow config are ignored. The caller’s scope wins. |
After any nested call returns, the caller’s scope is restored exactly as before.
ensure and cross-module rulesWhen you ensure a rule from another module, the runtime merges that module’s module-level config (agent.* / run.*) on top of the current environment (respecting locks). Workflow-level config does not apply to rules.
Same-module ensure keeps the caller’s environment as-is, so workflow-level overrides stay in place.
prompt steps use one of three backends:
agent.command (default cursor-agent) with stream-json output.claude on PATH. If the executable is missing, Jaiph reports an error and exits.OPENAI_API_KEY in the environment. If the key is missing, Jaiph reports an actionable error and exits.Backend-specific flags come from agent.cursor_flags / agent.claude_flags (or the matching env vars). The codex backend has no CLI flags; configure it with OPENAI_API_KEY and optionally JAIPH_CODEX_API_URL (defaults to https://api.openai.com/v1/chat/completions). There is no per-prompt backend override; the effective backend is whatever the config stack resolves to when the step runs.
Only the cursor backend consults agent.command. For claude and codex, Jaiph always invokes the Claude CLI or the Codex HTTP path (prompt.ts), regardless of agent.command.
When agent.backend is cursor (the default) and agent.command’s basename is anything other than cursor-agent, Jaiph treats it as a custom agent command. That lets you use a shell script, Python wrapper, or other CLI as a prompt backend — no need to implement the stream-json protocol.
How it works:
--output-format, --stream-partial-output, --workspace, etc.) are appended.Display: The run tree shows the command’s basename as the step name — e.g., prompt echo-wc.sh "..." instead of prompt cursor "...".
config {
agent.command = "./agents/my-agent.sh"
}
workflow default() {
answer = prompt "Summarize this codebase"
log "${answer}"
}
The custom agent script just reads stdin and prints its answer:
#!/usr/bin/env bash
input=$(cat)
# ... process the input ...
echo "Here is my summary: ..."
Custom commands still participate in the normal prompt lifecycle — PROMPT_START / PROMPT_END events are emitted, artifacts are written, and returns schema validation applies to the captured output.
config {
agent.backend = "codex"
agent.default_model = "gpt-4o"
}
workflow default() {
prompt "Explain this codebase"
}
Set the API key in your environment:
export OPENAI_API_KEY="sk-..."
jaiph run main.jh
The codex backend streams responses from the OpenAI API and supports structured returns schemas like the other backends. The default model is gpt-4o when agent.default_model is not set. To use a custom-compatible endpoint, set JAIPH_CODEX_API_URL.
When a prompt step runs, Jaiph resolves the effective model using this order:
agent.default_model / JAIPH_AGENT_MODEL is set and non-empty → use it.--model <name> is found inside the corresponding flags (agent.cursor_flags or agent.claude_flags) → use it. Codex has no flag channel for the model; only step 1 or 3 apply.gpt-4o in code when no explicit model is set (see Codex setup).agent.default_model applies to cursor, claude, and codex. For the Claude backend, when agent.default_model is set and agent.claude_flags does not already contain --model, Jaiph passes --model <value> to the Claude CLI automatically. If both are set, the value in agent.claude_flags takes precedence (it is appended last).
Diagnostics. Every prompt step records model metadata in PROMPT_START and PROMPT_END in run_summary.jsonl (model, model_reason):
{"type":"PROMPT_START","backend":"cursor","model":"gpt-4","model_reason":"explicit",...}
model_reason is one of: explicit (non-empty agent.default_model / JAIPH_AGENT_MODEL), flags (--model taken from agent.cursor_flags or agent.claude_flags), or backend-default (no resolved model string — Cursor/Claude binaries choose their own; codex also reports this when no model is configured, even though the HTTP client defaults to gpt-4o, so the model field may be omitted there). Inspect these events directly in the summary file.
No-model troubleshooting. If the backend rejects the auto-selected default, set agent.default_model (all backends). For cursor and claude you can also pass --model <name> in agent.cursor_flags / agent.claude_flags; codex has no flag channel — use agent.default_model or env JAIPH_AGENT_MODEL only.
jaiph testjaiph test never calls resolveRuntimeEnv. For a test_run_workflow step, the test runner builds a child env by spreading process.env, then sets JAIPH_TEST_MODE, JAIPH_WORKSPACE, JAIPH_RUNS_DIR (an ephemeral test path), JAIPH_SCRIPTS, and mock fields (JAIPH_MOCK_RESPONSES_FILE and/or JAIPH_MOCK_DISPATCH_SCRIPT) as needed. There is no CLI pass that pre-merges in-file config into that env; JAIPH_*_LOCKED flags are not set unless you export them in the parent environment yourself.
NodeWorkflowRuntime still layers module- and workflow-level in-file config with applyMetadataScope (same *_LOCKED rules: metadata wins only when the key is not locked in the current env). To pin agent settings in CI, set JAIPH_AGENT_* / JAIPH_RUNS_DIR / JAIPH_DEBUG in the environment, and/or keep config in the .jh module that defines the workflow you exercise. Note: jaiph run’s resolveRuntimeEnv resolves agent.trusted_workspace to an absolute path against the workspace; metadata-only merging uses the in-file string as given — for tests, a relative agent.trusted_workspace may end up in JAIPH_AGENT_TRUSTED_WORKSPACE as-is, so set an absolute path in env or config if you need parity with a normal run.
Quick reference for all in-file keys and their environment variable equivalents:
| In-file key | Environment variable |
|---|---|
agent.default_model |
JAIPH_AGENT_MODEL |
agent.command |
JAIPH_AGENT_COMMAND |
agent.backend |
JAIPH_AGENT_BACKEND |
agent.trusted_workspace |
JAIPH_AGENT_TRUSTED_WORKSPACE |
agent.cursor_flags |
JAIPH_AGENT_CURSOR_FLAGS |
agent.claude_flags |
JAIPH_AGENT_CLAUDE_FLAGS |
run.logs_dir |
JAIPH_RUNS_DIR |
run.debug |
JAIPH_DEBUG |
run.recover_limit |
(no env override) |
runtime.docker_image |
JAIPH_DOCKER_IMAGE |
runtime.docker_network |
JAIPH_DOCKER_NETWORK |
runtime.docker_timeout_seconds |
JAIPH_DOCKER_TIMEOUT |
module.name |
(no env override) |
module.version |
(no env override) |
module.description |
(no env override) |
There is no in-file key for the Codex HTTP endpoint or API key. Use environment only:
| Purpose | Environment variable |
|---|---|
| OpenAI-compatible API key (required for codex) | OPENAI_API_KEY |
| OpenAI-compatible chat-completions URL override | JAIPH_CODEX_API_URL |
Inside workflows, rules, and scripts, agent and run settings are visible as JAIPH_* environment variables. In orchestration strings, ${IDENTIFIER} resolves from workflow variables first, then from the process environment.
workflow default() {
log "backend=${JAIPH_AGENT_BACKEND} trusted_workspace=${JAIPH_AGENT_TRUSTED_WORKSPACE}"
}
The runtime also sets JAIPH_ARTIFACTS_DIR — the absolute path to the writable artifacts directory for the current run (.jaiph/runs/<run_id>/artifacts/ on the host, /jaiph/run/artifacts inside the Docker sandbox). The jaiphlang/artifacts library reads this variable; you can also use it directly in scripts. See Libraries — jaiphlang/artifacts.
JAIPH_DOCKER_* variables are not populated from in-file runtime.* inside the workflow runner process. Docker is configured when the CLI spawns the runner (or container). If you need Docker-related variables inside a script step, export them yourself or inherit them from the parent shell.
jaiph initjaiph init creates .jaiph/bootstrap.jh, writes .jaiph/SKILL.md from the skill file bundled with your installation (see JAIPH_SKILL_PATH in the CLI reference), and ensures .jaiph/.gitignore matches the canonical template (lists runs and tmp under .jaiph/). It does not add a separate config file — use config { ... } in your workflow sources.