Composable AI workflows you can trust.
curl -fsSL https://jaiph.org/run | bash -s 'workflow default() { const response = prompt "Say: Hello, I am [model name]!" log response}'
Installs Jaiph v0.9.3 to ~/.local/bin (if not already installed), and runs the sample workflow with Cursor CLI agent backend (the default one). See more samples!
Jaiph is under heavy development. Core features and workflow syntax are stable since v0.8.0, but you may expect breaking changes before v1.0.0.
Run the script below from the project directory:
curl -fsSL https://jaiph.org/init | bash
Installs Jaiph v0.9.3 to ~/.local/bin (if not
already installed), and runs jaiph init to initialize the Jaiph workspace in the
current directory.
curl -fsSL https://jaiph.org/install | bash
The installer will install the version 0.9.3 of Jaiph to
~/.local/bin. To switch versions, use jaiph use nightly
or jaiph use <version> to switch.
Or install from npm: npm install -g jaiph
Jaiph is a language and runtime for defining and orchestrating AI agent workflows.
It allows you to combine agentic workflows with strict checks and script calls. It comes with built-in Docker sandboxing for agentic workflows, and a set of tooling to make your development faster and more efficient.
Simple and easy to learn
Strict checks and structure over agent responses
Supports chaining, routing, agent inbox, and parallel workflows
Embed scripts in your favorite language
Built-in Docker sandboxing
Built-in testing framework
Tracks and saves all agent responses
Default formatter and VSCode plugin
No vendor lock-in: Use any AI agent you want
No data gathering, no tracking. Verify it in the source code!
Jaiph workflows are typically executable .jh files with a shebang.
The core primitives are rule (read-only checks), prompt (calling agents), script (custom code or shell commands) and workflow (the main unit of orchestration).
#!/usr/bin/env jaiph
# rules are executed on readonly filesystem
rule valid_name(name_arg) {
return match name_arg {
/[A-Z][a-z]+/ => name_arg
"" => fail "You didn't provide your name :("
_ => fail "You provided an invalid name :("
}
}
# workflows are main unit of orchestration
workflow default(name_arg) {
const name = ensure valid_name(name_arg)
# prompts call agents - cursor by default, but it's configurable
const response = prompt """
Say hello to ${name} and provide a fun fact about a person with the same name.
Respond with a single line. Do not inspect files or run tools.
"""
return response
}
Running the workflow:
➜ ./say_hello.jh Adam
Jaiph: Running say_hello.jh
workflow default (name_arg="Adam")
▸ rule valid_name (name_arg="Adam")
✓ rule valid_name (0s)
▸ prompt cursor "Say hello to ${name} and..." (name="Adam")
✓ prompt cursor (5s)
✓ PASS workflow default (5.1s)
Hello, Adam—Adam Smith, the 18th-century Scottish economist and philosopher, is often called the father of modern economics for his landmark work *The Wealth of Nations*.
When you don't provide the name parameter, the workflow fails:
➜ ./say_hello.jh
Jaiph: Running say_hello.jh
workflow default
▸ rule valid_name
✗ rule valid_name (0s)
✗ FAIL workflow default (0.4s)
Logs: <path>
Summary: <path>
out: <path>
err: <path>
Output of failed step:
You didn't provide your name :(
Jaiph also supports structured output. You can use the following syntax:
const response = prompt "..." returns "{ hello: string, fact: string }"
Then, Jaiph will add at runtime instructions to the prompt asking the agent to return a JSON
object with the fields hello and fact, and it will parse the response
and set the capture variable to the raw JSON string. The parser tolerates text before the JSON
object (e.g. when the agent writes preamble before {...}). You can access the
fields with dot notation: ${response.hello} and ${response.fact}.
Jaiph has native support for testing. All files that end with
*.test.jh are treated as tests.
And Jaiph highly recommends testing your workflows!
#!/usr/bin/env jaiph
import "say_hello.jh" as hello
# We expect this test to fail due to mismatch in error message
# between the prompt and the error message in the test file.
# We use it to verify the test works and the error message output
# is correct.
test "without name, workflow fails with validation message" {
# When
const response = run hello.default() allow_failure
# Then
expect_equal response "You didn't provide your name"
}
test "with name, returns greeting and logs response" {
# Given
const expected_response = "Hello Alice! Fun fact: Alice in Wonderland was written by Lewis Carroll."
mock prompt expected_response
# When
const response = run hello.default("Alice")
# Then
expect_equal response expected_response
}
Example failing test run output (expected string omits the trailing :( from stderr):
➜ ./say_hello.test.jh
testing say_hello.test.jh
▸ without name, workflow fails with validation message
✗ expect_equal failed: 0s
- You didn't provide your name
+ You didn't provide your name :(
▸ with name, returns greeting and logs response
✓ 0s
✗ 1 / 2 test(s) failed
- without name, workflow fails with validation message
The run … recover pattern is a first-class repair-and-retry loop.
When the target fails, the recover(failure) body runs, then the
target is retried automatically. The loop stops on success or when the retry
limit (default 10, configurable via run.recover_limit) is
exhausted. Below, an agent is asked to create the missing file so the next
attempt at the same script call passes.
#!/usr/bin/env jaiph
# scripts are defined in fenced blocks or single line backticks
# by default it's bash, but it can be any env: ```node, ```python3, etc.
script check_report_exists = ```
test -f report.txt
```
workflow default() {
# Recovery in loop: when check_report_exists() fails, the recovery body
# is executed to fix it, and then check_report_exists() is retried.
# By default, the retry limit is 10.
run check_report_exists() recover(failure) {
logerr "Failed to check report.txt"
prompt "report.txt is missing. Create it with a short dummy summary."
}
# scripts can be also executed inline
return run `cat report.txt`()
}
In the run below, check_report_exists() fails on the first
attempt. The recover body logs the failure and prompts an agent
to create report.txt; Jaiph then retries the script call which
now succeeds. The inline-backtick cat report.txt script returns
the file contents from the workflow, which Jaiph prints below the run tree.
➜ ./recover_loop.jh
Jaiph: Running recover_loop.jh
workflow default
▸ script check_report_exists
✗ script check_report_exists (0s)
! Failed to check report.txt
▸ prompt cursor "report.txt is missing. C..."
✓ prompt cursor (5s)
▸ script check_report_exists
✓ script check_report_exists (0s)
▸ script __inline_752d3a136cc9
✓ script __inline_752d3a136cc9 (0s)
✓ PASS workflow default (6.1s)
Summary
-------
This is a placeholder report. No build or test results were generated for
this file; it exists only to satisfy tooling or documentation that expects
`report.txt` to be present.
- Status: OK (dummy)
- Artifacts: none
- Next steps: replace with a real report when you add automated reporting
For one-shot failure handling without retry, use catch instead. See
Language — recover.
You can define named channels with inline routing — for instance
channel findings -> analyst means the analyst workflow
listens to messages passed on the findings channel. Each time a
message is sent to findings with the <- operator,
the analyst workflow is triggered.
#!/usr/bin/env jaiph
channel findings -> analyst
channel report -> reviewer
workflow scanner() {
log "Scanning for issues..."
findings <- "Found 3 issues in auth module"
}
workflow analyst(message, chan, sender) {
log "Analyzing message from ${sender} on channel ${chan}..."
report <- "Summary: ${message}"
}
workflow reviewer(message, chan, sender) {
log "Reviewing message from ${sender} on channel ${chan}..."
logerr "Critical issue: ${message}"
}
workflow default() {
run scanner()
}
Running the workflow:
➜ ./agent_inbox.jh
Jaiph: Running agent_inbox.jh
workflow default
▸ workflow scanner
· ℹ Scanning for issues...
✓ workflow scanner (0s)
▸ workflow analyst (message="Found 3 issues in auth module", chan="findings", sender="scanner")
· ℹ Analyzing message from scanner on channel findings...
✓ workflow analyst (0s)
▸ workflow reviewer (message="Summary: Found 3 issues in auth ...", chan="report", sender="analyst")
· ℹ Reviewing message from analyst on channel report...
· ! Critical issue: Summary: Found 3 issues in auth module
✓ workflow reviewer (0s)
✓ PASS workflow default (0s)
This sample runs two prompt workflows in parallel: one with Cursor and one with Claude.
Each workflow sets its own agent.backend, captures the prompt response, and logs it.
The default workflow uses run async to fan out both workflows concurrently.
Each run async response resolves on the first read, or at the end of
the embracing workflow.
#!/usr/bin/env jaiph
const prompt_text = "Say: Greetings! I am [model name]."
workflow cursor_say_hello(name) {
config { agent.backend = "cursor" }
const response = prompt "${prompt_text}"
log response
}
workflow claude_say_hello(name) {
config { agent.backend = "claude" }
const response = prompt "${prompt_text}"
log response
}
# surrounding workflow waits for all to complete
workflow default(name) {
run async cursor_say_hello(name)
run async claude_say_hello(name)
}
Running the workflow:
➜ ./async.jh
Jaiph: Running async.jh
workflow default
₁▸ workflow cursor_say_hello
₂▸ workflow claude_say_hello
₁· ▸ prompt cursor "Say: Greetings! I am [mo..."
₂· ▸ prompt claude "Say: Greetings! I am [mo..."
₁· ✓ prompt cursor (3s)
₁· ℹ Greetings! I am **Composer**, a language model trained by Cursor.
₁✓ workflow cursor_say_hello (3s)
₂· ✓ prompt claude (4s)
₂· ℹ Greetings! I am Claude Opus 4.6.
₂✓ workflow claude_say_hello (4s)
✓ PASS workflow default (4.6s)
Both workflows run in parallel, and in this case Cursor was faster than Claude.
./path/to/main.jh "first" "second" "third"
Jaiph runs the default workflow in the file and binds invocation arguments to its
parameters. Run artifacts and step output go under .jaiph/runs/.
.jaiph/ is the Jaiph working directory in your project—the recommended place for
workflow files. Run jaiph init so the project is configured correctly.
Combine prompts with strict checks. Compose rule,
script, ensure, and prompt in one executable flow so checks
and AI steps stay in the same pipeline. This way you can enforce structure over non-deterministic
agent responses.
See Grammar and Getting started
(jaiph.org/getting-started).
Embed and run scripts in the language of your choice. Define them with fence lang
tags providing the runtime: ```node, ```python3, ```ruby,
```pwsh etc.
Async calls. run async wf() returns a Handle<T>
that resolves on first read. Capture with const h = run async wf() and read
the handle when you need the value. Unresolved handles are implicitly joined
before the parent workflow completes. Supports recover and catch
composition for async error handling.
Agent inbox pattern (channels). Use inbox channels as a way to pass messages between
workflows. Declare channels at top level with channel <name> [-> workflow]
— routes are declared inline on the channel, not inside workflow bodies. Send with
channel_ref <- ...
(channel_ref can be local or imported, e.g. shared.findings).
See Inbox & Dispatch.
Failure recovery. ensure … catch and run … catch
handle failures inline: when a rule or script fails, the recovery body runs once.
For automatic repair-and-retry, use run … recover — a loop that retries
the target after each repair attempt (configurable limit, default 10). Both catch
and recover work in workflows. See Grammar.
Docker sandboxing. Workflows run inside Docker by default for local development,
providing
filesystem and process isolation for agent and shell actions. Disable with
JAIPH_UNSAFE=true. See Sandboxing.
Hooks. Attach shell automation to workflow and step lifecycle events via
~/.jaiph/hooks.json or <project>/.jaiph/hooks.json. See Hooks.
Custom agent backends. Point agent.command to any
executable — a shell script, a Python wrapper, or your own CLI tool — and Jaiph will
pipe the prompt via stdin and capture raw stdout as the response. No JSON stream
protocol required; just read stdin and print your answer.
Artifacts library. Publish files from inside the sandbox to a host-readable
location with the built-in jaiphlang/artifacts library (artifacts.save).
Works identically in Docker and on the host.
See Libraries.
Configuration. Control behavior with config { ... } blocks
at the module level or inside individual workflows for per-workflow overrides, plus environment
variables (env wins precedence). See Configuration and
CLI reference.
Testing Jaiph workflows. Test workflows with executable *.test.jh
suites, mocks, and assertions to cover deterministic and agent-assisted paths. See Testing.
Formatting. jaiph format rewrites .jh files to a canonical
style — consistent whitespace and indentation. See CLI
reference.
Jaiph source code is built mostly with real Jaiph workflows. The .jaiph/docs_parity.jh workflow runs documentation maintenance checks, changelog updates, and cross-doc consistency guards. The .jaiph/engineer.jh workflow implements a queue-driven engineering loop that picks work, implements changes, verifies CI, and updates queue state.
config { ... }agent.*
and run.* keys only; runtime.* and module.* are
module-level only). Environment variables override config values. See Configuration.import "file.jh" as alias · const name = value /
local name = value
const for new code)
shared by
rules, scripts, and workflows in the same file. Values can be single-line
"..." strings, triple-quoted """...""" multiline strings,
or bare tokens.
rule name() { ... } · rule name(params) { ... } ·
workflow name() { ... } · workflow name(params) { ... }
·
script name = `cmd` · script name = ```[lang] ... ```
rule is for reusable checks (Jaiph structured steps; used with
ensure),
workflow orchestrates Jaiph steps only, and script holds bash (or any
language via a fence lang tag like ```node, ```python3, or a custom
shebang) invoked with run. Rules and workflows require parentheses
on every definition — even when parameterless (e.g.
workflow default() { … }).
Named parameters go inside the parentheses; the compiler validates
call-site arity when the callee declares params. Any fence tag is valid — it maps directly
to
#!/usr/bin/env <tag>. Scripts run in full isolation
— only positional arguments
and essential Jaiph variables (JAIPH_SCRIPTS,
JAIPH_WORKSPACE) are inherited; module-scoped variables are not visible.
Reuse shell helpers with import script or small named script blocks in
the same module. Scripts are emitted as
separate executable files under scripts/ (within the run build output tree; see CLI reference).
ensure ref() · ensure ref(args) catch (name) { ... } ·
run ref() · run ref(args) catch (name) { ... } ·
run `body`(args)
ensure executes a rule (with optional args); catch handles
failure inline (runs once); run calls another workflow or module script
(managed step — same value/log contract as ensure). Parentheses are
required on all call sites — even when passing zero arguments
(e.g. run setup()).
Arguments can be quoted strings or bare identifiers:
run greet(name) is equivalent to run greet("${name}").
run `body`(args) embeds a one-off shell command directly
without a named script definition — supports arguments and capture.
Use triple backticks for multiline: run ```...```(args).
prompt "..." · prompt myVar ·
prompt """ ... """ ·
const name = prompt "..." returns "{ field: type }"
""" block for multiline text. Triple backticks are reserved for
scripts. Optionally validates structured JSON output and exposes captured variables.const name = … · const name = ensure ref() ·
const name = run ref()
const keyword.
All bindings are immutable — a name bound by a parameter,
const, capture, or script cannot be rebound in the same scope.
For ensure / run to rules and workflows,
explicit
return "value", return identifier
(or return run ref() /
return ensure ref()) feeds the variable; for run to a
script, capture follows
stdout. const RHS has stricter rules (no $(...) — use
run
to a script). A
call-like RHS must use the keyword explicitly — e.g.
const x = run helper(arg),
not
const x = helper(arg) (compile error with a correction hint). See
Immutable bindings and
Step output contract.
run async ref(args) ·
const h = run async ref(args)
Handle<T>
that resolves on first non-passthrough read (interpolation, passing as arg to
run, comparison, conditional). Passthrough (capture, re-assignment)
does not force resolution. Unresolved handles are implicitly joined at workflow exit.
Supports recover (retry loop) and catch (single-shot) composition:
run async foo() recover(err) { … }.
Workflows only. See Grammar and
Spec: Async Handles.
fail "reason" · fail """..."""fail aborts with stderr + non-zero exit. Use triple quotes for multiline messages.
ensure ref() catch (err) { … } ·
run ref() catch (err) { … }
catch clause). catch requires explicit bindings
in parentheses. Works in both workflows and rules.
run ref() recover (err) { … }run.recover_limit). recover
requires explicit bindings. Workflows only. See
Language.
match var { "lit" => … ⏎ /re/ => … ⏎ _ => … }$ or ${}). Arms are tested top-to-bottom; first match wins.
Patterns: string literal (exact), regex, or _ wildcard.
Arms are newline-delimited — commas between or after arms are rejected.
Usable as a statement, expression (const x = match var { … }),
or with return (return match var { … }).
Exactly one _ wildcard arm is required.
Arm bodies support single-line strings, triple-quoted """…""" multiline strings,
fail, run ref(…), and ensure ref(…).
These forms execute at runtime: fail aborts the workflow,
run/ensure execute the target and capture its return value.
The return keyword and inline scripts are forbidden inside arm bodies.
log "message" · log """...""" ·
logerr "message"
echo -e-style backslash escapes; structured
LOG / LOGERR payloads keep the raw message string.
channel <name> [-> workflow] ·
channel_ref <- ...
channel findings -> analyst) and send messages between workflows.
Routes belong on the channel declaration, not inside workflow bodies.
channel_ref supports local and imported refs (for example
findings, shared.findings). Undefined refs fail validation.
See Inbox &
Dispatch.
test "description" { ... }*.test.jh execute as test suites. See Testing.mock prompt "..." ·
mock prompt { /pattern/ => "response", _ => "default" }
mock workflow · mock rule · mock script
const name = run alias.workflow() · run alias.workflow()
· allow_failurerun and
optional const capture (same managed semantics as run
in .jh files): capture prefers an explicit return from the callee,
otherwise combined output with internal event lines stripped. Add allow_failure
when the test expects a non-zero exit. See Testing.
expect_equal · expect_contain ·
expect_not_contain