Jaiph

Start here Samples VSCode Agent skill

Prompts and any language, composed into repeatable programs.

Open Source·Powerful·Friendly!

Try it out!

curl -fsSL https://jaiph.org/run | bash -s 'export def main() {  const response = prompt "Say: Hello, I am [model name]!"  log response}'

Installs v0.15.0 to ~/.local/bin if needed, then runs the snippet with Cursor CLI (the default backend).

Jaiph is under heavy development. Expect breaking changes before v1.0.0.

curl -fsSL https://jaiph.org/init | bash

Installs Jaiph if needed, then runs jaiph init in this directory.

curl -fsSL https://jaiph.org/install | bash

Writes v0.15.0 to ~/.local/bin. Switch later with jaiph use nightly or jaiph use <version>.

irm https://jaiph.org/install.ps1 | iex

Installs v0.15.0 to %LOCALAPPDATA%\jaiph\bin and adds it to your PATH. Then run a sample below, or jaiph init in a project directory.

Jaiph is under heavy development. Expect breaking changes before v1.0.0.

What is Jaiph?

Jaiph is a polyglot orchestration language and runtime. It turns the prompts and scripts (Bash, Python, Node, and others) an agent would otherwise chain ad hoc into a repeatable, testable workflow.

One Jaiph file can run as a command-line app, an MCP server over stdio, or an HTTP API with OpenAPI and HTTP MCP, with explicit secret grants and recorded outputs.

Start here

Why Jaiph?

Language

Simple, small, easy to learn

Reusable workflows instead of one-off agent command chains

Named channels between steps. Parallel runs

Compose prompts with scripts in any language

Tooling

One file: CLI, stdio MCP, or HTTP API with MCP

Runs on your machine. Put it in a container if you want a sandbox

Built-in tests with prompt mocks

Every agent response is saved

Open Source

No vendor lock-in. Use any agent.

No tracking. Read the source.

Samples

A .jh file is a program. def is a procedure, prompt calls an agent, script runs code. jaiph run enters at export def main.

#!/usr/bin/env jaiph

# check the name before calling the agent
def valid_name(name_arg) {
  return match name_arg {
    /[A-Z][a-z]+/ => name_arg
    _ => fail "You provided an invalid name :("
  }
}

# export def main is the jaiph run entry
export def main(name_arg) {
  const name = 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 file:

➜  ./say_hello.jh Adam

Jaiph: Running say_hello.jh

def main (name_arg="Adam")
  ▸ def valid_name (name_arg="Adam")
  ✓ def valid_name (0s)
  ▸ prompt cursor "Say hello to ${name} and..." (name="Adam")
  ✓ prompt cursor (5s)

✓ PASS def main (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*.

Missing name fails before the prompt:

➜  ./say_hello.jh

Jaiph: Running say_hello.jh

def main
  ▸ def valid_name
  ✗ def valid_name (0s)

✗ FAIL def main (0.4s)
  Logs: <path>
  Summary: <path>
    out: <path>
    err: <path>

  Output of failed step:
    You provided an invalid name :(

Need a JSON shape? prompt "..." returns "{ hello: string, fact: string }" then read ${response.hello}.

Native tests. Files named *.test.jh are test suites. Mock prompts so runs stay deterministic.

The first test is written to fail on purpose: the expected string drops the trailing :(, so you can see what a miss looks like. The second test passes.

#!/usr/bin/env jaiph

import "say_hello.jh" as hello

# We expect this test to fail due to mismatch in error message
# between the actual failure and the expected string in the test file:
# the expected string drops the trailing ":(". We use it to verify the
# test works and the error message output is correct.
test "without name, main fails with validation message" {
  # When
  const response = hello.main() allow_failure

  # Then
  expect_equal response "You provided an invalid 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 = hello.main("Alice")

  # Then
  expect_equal response expected_response
}

That first test's output:

➜  ./say_hello.test.jh
testing say_hello.test.jh
  ▸ without name, main fails with validation message
  ✗ expect_equal failed: 0s
    - You provided an invalid name
    + You provided an invalid name :(

  ▸ with name, returns greeting and logs response
  ✓ 0s

✗ 1 / 2 test(s) failed
  - without name, main fails with validation message

When a step fails, recover repairs it and retries. Default limit is 10.

#!/usr/bin/env jaiph

# scripts are defined in fenced blocks or one-line single quotes
# by default it's bash, but it can be any env: '''node, '''python3, etc.
script check_report_exists = '''
  test -f report.txt
'''

export def main() {
  # 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.
  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 'cat report.txt'()
}

First attempt fails, the agent creates the file, retry passes. For a one-shot handler, use catch.

➜  ./recover_loop.jh

Jaiph: Running recover_loop.jh

def main
  ▸ 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 def main (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

Named channels route messages. channel findings -> analyst runs analyst on every send.

#!/usr/bin/env jaiph

channel findings -> analyst
channel report -> reviewer

def scanner() {
  log "Scanning for issues..."
  send "Found 3 issues in auth module" -> findings
}

def analyst(msg) {
  send "Summary: ${msg}" -> report
}

def reviewer(msg) {
  logerr "Critical issue: ${msg}"
}

export def main() {
  scanner()
}

Running the file:

➜  ./agent_inbox.jh

Jaiph: Running agent_inbox.jh

def main
  ▸ def scanner
  ·   ℹ Scanning for issues...
  ✓ def scanner (0s)
  ▸ def analyst (msg="Found 3 issues in auth module")
  ✓ def analyst (0s)
  ▸ def reviewer (msg="Summary: Found 3 issues in auth ...")
  ·   ! Critical issue: Summary: Found 3 issues in auth module
  ✓ def reviewer (0s)

✓ PASS def main (0s)

Two prompts in parallel, Cursor and Claude. async fans out; the parent waits for both.

#!/usr/bin/env jaiph

const prompt_text = "Say: Greetings! I am [model name]."

def cursor_say_hello(name) {
  config { agent.backend = "cursor" }
  const response = prompt "${prompt_text}"
  log response
}

def claude_say_hello(name) {
  config { agent.backend = "claude" }
  const response = prompt "${prompt_text}"
  log response
}

# surrounding def waits for all to complete
export def main(name) {
  async cursor_say_hello(name)
  async claude_say_hello(name)
}

Running the file:

➜  ./async.jh

Jaiph: Running async.jh

def main
 ₁▸ def cursor_say_hello
 ₂▸ def 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.
 ₁✓ def cursor_say_hello (3s)
 ₂·   ✓ prompt claude (4s)
 ₂·   ℹ Greetings! I am Claude Opus 4.6.
 ₂✓ def claude_say_hello (4s)

✓ PASS def main (4.6s)

Cursor finished first in this run.

Jaiph supports streaming of stdin pipelines so scripts can produce a lot of data without loading it all into memory. Chain stages with ->. The first call produces the bytes; every stage after it is a script that reads stdin and writes stdout. A const at the end names the last stage's result.

#!/usr/bin/env jaiph

# Jaiph streams stdin pipelines so a script can produce a lot of data
# without loading it all into memory. Chain stages with ->.
script gen = '''bash
  printf 'alpha\nbeta\ngamma\n'
'''

script upper = '''bash
  tr 'a-z' 'A-Z'
'''

script count = '''bash
  wc -l | tr -d ' '
'''

export def main() {
  # generate lines -> uppercase them -> reduce to a line count
  const n = stdin gen() -> upper() -> count()
  return n
}

Each stage is its own step; main returns the line count:

➜  ./stdin.jh

Jaiph: Running stdin.jh

def main
  ▸ script gen
  ·   ▸ script upper
  ·   ·   ▸ script count
  ✓ script gen (0s)
  ·   ✓ script upper (0s)
  ·   ·   ✓ script count (0s)

✓ PASS def main (0.1s)

3