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

What is 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.

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

Tooling

Built-in Docker sandboxing

Built-in testing framework

Tracks and saves all agent responses

Default formatter and VSCode plugin

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

# 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 (Docker sandbox, fusefs)

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 (Docker sandbox, fusefs)

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 (Docker sandbox, fusefs)

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 (Docker sandbox, fusefs)

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 (Docker sandbox, fusefs)

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"

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.

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

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.

Runtime

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.

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, module metadata). Allowed at the top level (module-wide) and inside individual workflows (per-workflow overrides for 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
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_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 }"
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. 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)
Run a workflow or script concurrently. Returns a 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) { … }
Failure recovery: when the target fails, the recovery body runs once (like a catch clause). catch requires explicit bindings in parentheses. Works in both workflows and rules.
run ref() recover (err) { … }
Repair-and-retry loop: when the target fails, the repair body runs and the target is retried automatically. Stops on success or when the retry limit is exhausted (default 10, configurable via run.recover_limit). recover requires explicit bindings. Workflows only. See Language.
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. 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"
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).