Prompts and any language, composed into repeatable programs.
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.
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.
Five minutes. No API keys. Write a file and watch the run tree.
jaiph mcp ./tools.jh — Claude Code, Cursor, or any MCP client.
Add a prompt. Cursor, Claude, or Codex — you pick the backend.
jaiph serve ./tools.jh — REST, OpenAPI, Swagger UI, and HTTP MCP.
Mocks, assertions, *.test.jh. CI-friendly and deterministic.
Defs, prompts, scripts, recover, channels. The full lookup.
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
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
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