Jaiph

Start here Samples VSCode Agent skill

Composable AI programs you can trust.

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

What is Jaiph?

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.

Start here

Why Jaiph?

Language

Simple, small, easy to learn

Checks around agent output

Named channels between steps. Parallel runs

Embed scripts in any language

Tooling

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

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