Composable AI programs you can trust.
curl -fsSL https://jaiph.org/run | bash -s 'export def main() { const response = prompt "Say: Hello, I am [model name]!" log response}'
Installs v0.14.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.14.0 to ~/.local/bin. Switch later
with jaiph use nightly or jaiph use <version>.
irm https://jaiph.org/install.ps1 | iex
Installs v0.14.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 language and runtime for defining and orchestrating AI agent calls, with strict checks and custom scripts.
You write a Jaiph file, and you can run it as a command-line agentic app, an MCP server, or an HTTP server, with isolation of secrets 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 — OpenAPI, Swagger UI, same defs.
Mocks, assertions, *.test.jh. CI-friendly and deterministic.
Defs, prompts, scripts, recover, channels. The full lookup.
Simple, small, easy to learn
Checks around agent output
Named channels between steps. Parallel runs
Embed scripts in any language
One file: CLI, MCP server, or HTTP API
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 didn't provide your name :("
_ => fail "You provided an invalid name :("
}
}
# export def main is the jaiph run entry
export def main(name_arg) {
const name = run 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 didn't provide your 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 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, main fails with validation message" {
# When
const response = run hello.main() 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.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 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, 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 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
```
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.
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`()
}
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() {
run 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. run 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) {
run async cursor_say_hello(name)
run 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.