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.0 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.
curl -fsSL https://jaiph.org/install | bash
The installer will install the version 0.9.0 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
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 reporting
Built-in testing framework
Tracks and saves all agent responses
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
# scripts are defined in fenced blocks or single line backticks
# by default it's bash, but it cany be any env: ```node, ```python3, etc.
script validate_name = ```
if [ -z "$1" ]; then
echo "You didn't provide your name :(" >&2
exit 1
fi
```
# rules are always executed on readonly filesystem
rule name_was_provided(name) {
run validate_name(name)
}
# workflows are main unit of orchestration
workflow default(name) {
ensure name_was_provided(name)
# 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.
"""
log response
}
Running the workflow:
➜ ./say_hello.jh Jakub
Jaiph: Running say_hello.jh
workflow default (name="Jakub")
▸ rule name_was_provided (name="Jakub")
· ▸ script validate_name (1="Jakub")
· ✓ script validate_name (0s)
✓ rule name_was_provided (0s)
▸ prompt cursor "Say hello to Jakub and p..." (name="Jakub")
✓ prompt cursor (3s)
ℹ Hello Jakub — Czech conductor Jakub Hrůša has led major orchestras including the Bamberg Symphony and often appears at the BBC Proms.
✓ PASS workflow default (4.1s)
When you don't provide the name parameter, the workflow fails:
➜ ./say_hello.jh
Jaiph: Running say_hello.jh
workflow default
▸ rule name_was_provided
· ▸ script validate_name
· ✗ script validate_name (0s)
✗ rule name_was_provided (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
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
mock prompt "Hello Alice! Fun fact: Alice in Wonderland was written by Lewis Carroll."
# When
run hello.default("Alice")
}
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 ensure ... recover pattern checks a rule and, on failure,
runs a recovery block. The recover (failure) binding captures
the merged stdout+stderr from the failed check.
Recovery runs once — for retries, the workflow calls itself
recursively (run default()).
#!/usr/bin/env jaiph
# Recursive recovery: when a check fails, prompt an agent to fix it,
# then retry via run default(). Jaiph CI uses the same pattern to
# auto-fix failing tests — see .jaiph/ensure_ci_passes.jh
script check_report = `test -f report.txt`
rule report_exists() {
run check_report()
}
workflow default() {
ensure report_exists() recover (failure) {
prompt "report.txt is missing. Create it with a short dummy summary."
run default()
}
}
In the run below, report_exists fails once. The agent creates
report.txt, and the recursive run default() retries
successfully.
➜ ./recover_loop.jh
Jaiph: Running recover_loop.jh
workflow default
▸ rule report_exists
· ▸ script check_report
· ✗ script check_report (0s)
✗ rule report_exists (0s)
▸ prompt cursor "report.txt is missin..."
✓ prompt cursor (5s)
▸ workflow default
· ▸ rule report_exists
· · ▸ script check_report
· · ✓ script check_report (0s)
· ✓ rule report_exists (0s)
✓ workflow default (0.1s)
✓ PASS workflow default (5.5s)
Jaiph's own CI uses this same pattern to auto-fix failing tests — see
.jaiph/ensure_ci_passes.jh.
The inbox system lets workflows communicate through named channels.
A workflow sends a message with <- (RHS: quoted literal, ${var}, or
run to a script) and an inline route on the channel declaration
(channel name -> workflow) dispatches it to receivers with
the dispatch values (message, channel, sender) bound to the target's declared parameters.
#!/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 aut...", 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, with an
implicit join before completion.
#!/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"
Arguments are bound to named parameters declared on the default workflow
(e.g. workflow default(task) → ${task}).
When you call a workflow, it executes the workflow default entrypoint.
By convention, keep Jaiph workflow files in <project_root>/.jaiph/ so
workspace-root detection and agent setup stay predictable.
Jaiph stores run artifacts in .jaiph/runs/ (step .out/.err
logs
plus run_summary.jsonl, an append-only JSONL event stream for reporting). Keep those
out of
git: jaiph init writes .jaiph/.gitignore (listing runs and
tmp) so ephemeral data stays untracked while workflows remain committable. See the
CLI reference for event types, field requirements, and
correlation rules.
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).
Use Bash (or any language) in scripts. Command pipelines, checks, and background
jobs
belong in script definitions; workflows and rules call them with run. Use
fence lang tags (```node, ```python3, ```ruby, etc.) to select
an interpreter without writing a shebang line — any tag is valid, mapping directly to
#!/usr/bin/env <tag>. If no tag is present, add a manual
#! shebang as the first body line.
For trivial one-off commands, use inline scripts:
run `echo ok`() — no named block needed.
Async calls. For async managed work, use run async wf() — Jaiph fans
out the workflows concurrently and implicitly joins them before the parent workflow
completes.
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 … recover and run … recover
handle failures inline: when a rule or script fails, the recovery body runs once
(like a catch clause). For retries, use explicit recursion. Both forms work in workflows
and rules. See Grammar.
Reporting server. Run jaiph report --workspace . to open a local
read-only dashboard on .jaiph/runs: run history, step tree, log previews, raw
.out/.err streaming, and an aggregate view, with live updates while
workflows append run_summary.jsonl. See Reporting server.
Docker sandboxing. Enable isolated execution with Docker for stronger containment of
agent and shell actions. Configure in config { runtime.* }. 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.
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. Imports, config, and channels are hoisted
to the top; all other definitions (const, rules, scripts, workflows, tests) keep their source-file
order. Use --check in CI to catch drift without modifying files, and
--indent <n> to set spaces per level.
Formatting is idempotent; comments, shebangs, and intentional blank lines between
steps are preserved. Comments before hoisted constructs move with them. Multiline string and
script bodies are emitted verbatim — inner lines are never re-indented. 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). 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_LIB, JAIPH_SCRIPTS,
JAIPH_WORKSPACE) are inherited; module-scoped variables are not visible.
Use source "$JAIPH_LIB/…" for shared utilities. Scripts are emitted as
separate executable files under scripts/ (within the run build output tree; see CLI reference).
ensure ref() · ensure ref(args) recover (name) { ... } ·
run ref() · run ref(args) recover (name) { ... } ·
run `body`(args)
ensure executes a rule (with optional args); recover handles
failure inline (runs once, like a catch clause); 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.
For ensure / run to rules and workflows,
explicit
return "value" (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
Step output contract.
run async ref(args)const x = run async ...) is not supported.
See Grammar.
fail "reason" · fail """..."""fail aborts with stderr + non-zero exit. Use triple quotes for multiline messages.ensure ref() recover (err) { … } ·
run ref() recover (err) { … }
catch clause). recover requires explicit bindings
in parentheses. Works in both workflows and rules. For retries, use explicit
recursion in the recovery body.
match var { "lit" => …, /re/ => …, _ => … }$ or ${}). Arms are tested top-to-bottom; first match wins.
Patterns: string literal (exact), regex, or _ wildcard.
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