Your first agent run

This tutorial builds on Your first run. You already have the jaiph CLI on your PATH, you have run a file that used only script steps, and you have looked at the artifacts under .jaiph/runs/. Here you will add a prompt step that calls an agent backend. jaiph run executes on the host, so a cursor or claude prompt that can run tools runs as the same operating system user as jaiph. Jaiph hands each agent a reduced set of environment variables, but that is not a process sandbox. If you want a sandbox, wrap jaiph in your own container or CI runner. See Deploy jaiph.

What you will build

You will build a file with two steps. The first step runs a def that checks a name with match. The second step is a prompt that asks an agent to greet that name.

Credentials

A prompt step calls an agent backend. Before it spawns the runner, the CLI runs a credential pre-flight. The cursor backend warns and proceeds when CURSOR_API_KEY is missing, because a stored CLI login might still work. The claude backend is not checked — a stored Claude CLI login is enough. The codex backend has no login fallback, so a missing OPENAI_API_KEY is a hard error (E_AGENT_CREDENTIALS).

Pick one backend and set its env var on the host:

# Cursor (the default backend if nothing else is configured)
export CURSOR_API_KEY="..."

# Claude
export ANTHROPIC_API_KEY="sk-ant-..."
# or, after running `claude setup-token`:
export CLAUDE_CODE_OAUTH_TOKEN="..."

# Codex (OpenAI)
export OPENAI_API_KEY="sk-..."

For the full table of what each backend needs, see Authenticate agent backends.

1. Configure the backend (optional)

If you set CURSOR_API_KEY above, you can skip this step, because cursor is the default backend. Otherwise, add a module-level config { … } block at the top of your file so the run uses the backend you want.

The full file you build in this tutorial is:

config {
  agent.backend = "claude"
  agent.model = "sonnet"
}

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

export def main(name_arg) {
  const name = valid_name(name_arg)

  const response = prompt """
    Say hello to ${name} and add one fun fact about a person with the same name.
    Respond with a single line. Do not inspect files or run tools.
  """

  return response
}

The file uses some syntax that Your first run did not:

Save the file as greet.jh.

2. Run it

jaiph run ./greet.jh "Adam"

The CLI does a few things before any step runs:

  1. Loads the module graph. It parses the entry file, which is the only file in this tutorial.
  2. Runs the credential pre-flight for the selected backend.
  3. Prints the banner, then validates the module and emits its scripts. After that it spawns the host runner.

You should see the following, though timings, model output, and the exact step name will differ:

Jaiph: Running greet.jh

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

✓ PASS def main (5.1s)

Hello, Adam! Adam Smith, the 18th-century Scottish economist, is often called the father of modern economics.

The banner and the step lines each tell you something:

3. Make valid_name reject a bad name

Re-run with an empty string so valid_name falls through to the _ arm:

jaiph run ./greet.jh ""

The output ends with the failure footer:

  ▸ def valid_name
  ✗ def valid_name (0s)

✗ FAIL def main (0.3s)
  Logs: …/.jaiph/runs/…
  Summary: …/run_summary.jsonl
    out: …/000002-def__valid_name.out
    err: …/000002-def__valid_name.err

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

The prompt step never runs, because valid_name failed. The .err file holds the text shown under Output of failed step:.

4. Inspect the prompt record

Each prompt step writes a PROMPT_START and a PROMPT_END line to run_summary.jsonl. Filter the latest run:

jq -c 'select(.type=="PROMPT_START")' .jaiph/runs/*/*/run_summary.jsonl | tail -1

The record includes the resolved backend, the model (or null when the backend picks its own), and a model_reason. For this run the reason is explicit, because you set agent.model. The other values are flags, backend-default, and none. Configure backend & model describes each value and reads the same fields to check config precedence.

Where to go next

You now have a working agent run. Here are some directions to go next: