Jaiph

Samples GitHub VSCode Agent skill

Composable AI workflows you can trust.

Open Source·Powerful·Friendly!

Try it out!

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

Why Jaiph?

Language

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

Runtime

Built-in Docker sandboxing

Built-in reporting

Built-in testing framework

Tracks and saves all agent responses

Open Source

No vendor lock-in: Use any AI agent you want

No data gathering, no tracking. Verify it in the source code!

Samples

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.

Core features

Running a workflow

./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.

Language

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.

Runtime

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.

Samples

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.

Syntax

Jaiph workflows

config { ... }
Optional runtime options (agent backend/flags, logs, runtime). Allowed at the top level (module-wide) and inside individual workflows (per-workflow overrides for agent.* and run.* keys). Environment variables override config values. See Configuration.
import "file.jh" as alias · const name = value / local name = value
Import other modules and define module-scoped variables (prefer 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 }"
Sends a prompt to the configured agent. The body can be a single-line string, a bare identifier, or a triple-quoted """ 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()
Bind or capture step values. All captures require the 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)
Run a workflow or script concurrently. All async steps are implicitly joined before the workflow completes; failures are aggregated. Workflows only — capture (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) { … }
Failure recovery: when the target fails, the recovery body runs once (like a 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/ => …, _ => … }
Pattern match on a string value. The subject is a bare identifier (no $ 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"
Emit informational or error output to the run tree while keeping workflow execution explicit and auditable. Use triple quotes for multiline messages. Terminal and tree text use echo -e-style backslash escapes; structured LOG / LOGERR payloads keep the raw message string.
channel <name> [-> workflow] · channel_ref <- ...
Declare channels at top level with optional inline routing (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.

Jaiph tests

test "description" { ... }
Defines a test case; files named *.test.jh execute as test suites. See Testing.
mock prompt "..." · mock prompt { /pattern/ => "response", _ => "default" }
Stub prompt calls with fixed responses or pattern-based dispatch so tests stay deterministic.
mock workflow · mock rule · mock script
Replace workflow, rule, or script implementations in tests for isolation and targeted assertions.
const name = run alias.workflow() · run alias.workflow() · allow_failure
Runs an imported workflow through the test harness with explicit run 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
Core assertion helpers for exact matches and substring checks (snake_case).