From jetty
Manage Jetty workflows and assets. Use when the user wants to create, edit, run, deploy, debug, or monitor AI/ML workflows on Jetty. Also use when they mention collections, tasks, trajectories, datasets, models, labels, step templates, or workflow runs. Triggers include 'run workflow', 'create task', 'list collections', 'check trajectory', 'label trajectory', 'add label', 'deploy workflow', 'show results', 'download output', 'debug run', 'workflow failed', or any Jetty/mise/dock operations. Even if the user doesn't say 'Jetty' explicitly, use this skill whenever they're working with Jetty API endpoints, workflow JSON, or init_params.
How this skill is triggered — by the user, by Claude, or both
Slash command
/jetty:jetty [command] [args][command] [args]This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Before doing any work, ask the user which collection to use via AskUserQuestion (header: "Collection", question: "Which Jetty collection should I use?"). Skip if you already know the collection from context.
README.mdexamples/document-qa.jsonexamples/fan-out-analysis.jsonexamples/simple-chat.jsonjetty-cli.shreferences/agents-and-models.mdreferences/batch-runs.mdreferences/step-templates.mdreferences/workflow-templates.mdscripts/jetty_auth.pytemplates/batch-processor.jsontemplates/cute-feline-detector-gemini.jsontemplates/cute-feline-detector-openai.jsontemplates/document-summarizer.jsontemplates/image-generation.jsontemplates/model-comparison.jsontemplates/quick-chat.jsontemplates/simple-chat.jsontemplates/text-echo.jsontemplates/text-evaluation.jsonBefore doing any work, ask the user which collection to use via AskUserQuestion (header: "Collection", question: "Which Jetty collection should I use?"). Skip if you already know the collection from context.
| Service | Base URL | Purpose |
|---|---|---|
| Jetty API | https://flows-api.jetty.io | All operations: workflows, collections, tasks, datasets, models, trajectories, files |
| Frontend | https://jetty.io | Web UI only — do NOT use for API calls |
For endpoints not documented in this skill, the Jetty API serves a live OpenAPI spec and Swagger UI:
| Endpoint | What it gives you |
|---|---|
https://flows-api.jetty.io/openapi.json | Machine-readable OpenAPI 3 spec — request/response schemas, parameter types, examples |
https://flows-api.jetty.io/docs | Interactive Swagger UI for the same spec — useful when probing an unfamiliar endpoint |
Both are reachable without authentication. When this skill's docs don't mention an endpoint you need, check the OpenAPI spec first — it's the ground truth.
When sharing links with the user (e.g., after launching a run), use these exact URL patterns. Do NOT guess or invent URL paths — only use the formats listed here:
| What | URL Pattern | Example |
|---|---|---|
| Task (all trajectories) | https://jetty.io/{COLLECTION}/{TASK} | https://jetty.io/jettyio/figma-draw |
| Single trajectory | https://jetty.io/{COLLECTION}/{TASK}/{TRAJECTORY_ID} | https://jetty.io/jettyio/figma-draw/aa7e4430 |
| Collection overview | https://jetty.io/{COLLECTION} | https://jetty.io/jettyio |
Read the API token from ~/.config/jetty/token and set it as a shell variable at the start of every bash block.
TOKEN="$(cat ~/.config/jetty/token 2>/dev/null)"
If the file doesn't exist, check CLAUDE.md for a token starting with mlc_ (legacy location) and migrate it:
mkdir -p ~/.config/jetty && chmod 700 ~/.config/jetty
printf '%s' "$TOKEN" > ~/.config/jetty/token && chmod 600 ~/.config/jetty/token
Security rules:
mlc_...xxxx)API keys are scoped to specific collections.
The mlc_ API key is collection-scoped (it resolves to the org that owns the
collection). Subscription credentials (Nous / Codex / Anthropic, under
Settings → Connected Accounts) are user-scoped to your Clerk user. So to
link or run on a personal subscription from the CLI, you must act as that
user — an mlc_ key can't see your linked accounts.
scripts/jetty_auth.py does a browser login (Clerk OAuth, Authorization Code +
PKCE, localhost loopback) and stores a refreshable user token at
~/.config/jetty/user-token.json (separate from the mlc_ key):
JA="$(dirname "$0")/scripts/jetty_auth.py" # or the skill's scripts/jetty_auth.py
python3 "$JA" login # browser login as your Clerk user
python3 "$JA" whoami # show sub / email / azp
python3 "$JA" accounts # list your linked subscriptions
python3 "$JA" connect nous # paste a Portal refresh token (hermes setup --portal)
python3 "$JA" token # print a fresh access token (auto-refreshes)
python3 "$JA" logout
Which token to use:
/connected-accounts/*) and running
a task on a subscription — must use the user token:
AUTH="Bearer $(python3 "$JA" token)".mlc_ key as above.Run a runbook on a connected subscription (user identity + the
subscription_credential param):
python3 "$JA" login # once
TOK="$(python3 "$JA" token)"
curl -s -X POST -H "Authorization: Bearer $TOK" \
-F 'init_params={"subscription_credential":"nous"}' \
"https://flows-api.jetty.io/api/v1/run/{COLLECTION}/{TASK}"
Requires the Clerk "Jetty CLI" OAuth app (provisioned) and mise accepting its
azp. Config is env-overridable (JETTY_CLERK_CLIENT_ID, JETTY_CLERK_ISSUER,
JETTY_API). See the "CLI login via Clerk OAuth" design doc on the Subscription
Credential Forwarding project for the full architecture.
In all examples: TOKEN="$(cat ~/.config/jetty/token)" must be set first.
# List all collections
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/collections/" | jq
# Get collection details
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/collections/{COLLECTION}" | jq
# Create a collection
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/api/v1/collections/" \
-d '{"name": "my-collection", "description": "My workflows"}' | jq
# List tasks
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/tasks/{COLLECTION}/" | jq
# Get task details (includes workflow definition)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/tasks/{COLLECTION}/{TASK}" | jq
# Search tasks
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/tasks/{COLLECTION}/search?q={QUERY}" | jq
# Create task
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/api/v1/tasks/{COLLECTION}" \
-d '{
"name": "my-task",
"description": "Task description",
"workflow": {
"init_params": {},
"step_configs": {},
"steps": []
}
}' | jq
# Update task
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/api/v1/tasks/{COLLECTION}/{TASK}" \
-d '{"workflow": {...}, "description": "Updated"}' | jq
# Delete task
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/tasks/{COLLECTION}/{TASK}" | jq
# Run async (returns immediately with workflow_id)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-F 'init_params={"key": "value"}' \
"https://flows-api.jetty.io/api/v1/run/{COLLECTION}/{TASK}" | jq
# Run sync (waits for completion — use for testing, not production)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-F 'init_params={"key": "value"}' \
"https://flows-api.jetty.io/api/v1/run-sync/{COLLECTION}/{TASK}" | jq
# Run with file upload (must use -F multipart, not -d JSON)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-F 'init_params={"prompt": "Analyze this document"}' \
-F "files=@/path/to/file.pdf" \
"https://flows-api.jetty.io/api/v1/run/{COLLECTION}/{TASK}" | jq
Before triggering a run, check if the collection is on an active trial with no provider keys configured:
TOKEN="$(cat ~/.config/jetty/token)"
# Check trial status
TRIAL=$(curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/trial/{COLLECTION}")
TRIAL_ACTIVE=$(echo "$TRIAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('active', False))")
# Check if provider keys exist
COLL=$(curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/collections/{COLLECTION}/environment")
HAS_KEYS=$(echo "$COLL" | python3 -c "
import sys, json
d = json.load(sys.stdin)
evars = d.get('environment_variables', {})
keys = ['OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'REPLICATE_API_TOKEN']
print(any(k in evars for k in keys))
")
If the trial is active and no provider keys are configured (HAS_KEYS is False), include use_trial_keys: true in the run request body:
# Run with trial keys
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-F 'init_params={"key": "value"}' \
-F 'use_trial_keys=true' \
"https://flows-api.jetty.io/api/v1/run/{COLLECTION}/{TASK}" | jq
After triggering a run, if the response includes trial metadata (e.g., trial object with runs_used, runs_limit, minutes_remaining), display it to the user:
Trial run {runs_used}/{runs_limit} -- {minutes_remaining} minutes remaining
If runs_remaining is 2 or fewer, show a warning:
Warning: {runs_remaining} trial runs left. Run
/jetty-setupto add your own API keys.
# Example: parse trial metadata from run response
RESPONSE=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-F 'init_params={"key": "value"}' \
"https://flows-api.jetty.io/api/v1/run/{COLLECTION}/{TASK}")
echo "$RESPONSE" | python3 -c "
import sys, json
d = json.load(sys.stdin)
trial = d.get('trial')
if trial:
used = trial.get('runs_used', '?')
limit = trial.get('runs_limit', '?')
remaining = trial.get('runs_remaining', '?')
mins = trial.get('minutes_remaining', '?')
print(f'Trial run {used}/{limit} -- {mins} minutes remaining')
if isinstance(remaining, int) and remaining <= 2:
print(f'Warning: {remaining} trial runs left. Run /jetty-setup to add your own API keys.')
"
# List trajectories — response is {"trajectories": [...], "total", "page", "limit", "has_more"}
# Access the array via .trajectories, NOT the top-level object
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/db/trajectories/{COLLECTION}/{TASK}?limit=20" | jq '.trajectories'
# Get single trajectory (steps are an object keyed by name, not an array)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/db/trajectory/{COLLECTION}/{TASK}/{TRAJECTORY_ID}" | jq
# Get workflow logs
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/workflows-logs/{WORKFLOW_ID}" | jq
# Get statistics
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/db/stats/{COLLECTION}/{TASK}" | jq
# Download a generated file — path from trajectory: .steps.{STEP}.outputs.images[0].path
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/file/{FULL_FILE_PATH}" -o output_file.jpg
# Batch update — valid statuses: pending, completed, failed, cancelled, archived
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/api/v1/trajectory/{COLLECTION}/{TASK}/statuses" \
-d '{"TRAJECTORY_ID": "cancelled"}' | jq
# Add a label to a trajectory
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/api/v1/trajectory/{COLLECTION}/{TASK}/{TRAJECTORY_ID}/labels" \
-d '{"key": "quality", "value": "high", "author": "[email protected]"}' | jq
Label fields: key (required), value (required), author (required).
For the full catalog, read references/step-templates.md.
# List all available step templates
curl -s "https://flows-api.jetty.io/api/v1/step-templates" | jq '[.templates[] | .activity_name]'
# Get details for a specific activity
curl -s "https://flows-api.jetty.io/api/v1/step-templates" | jq '.templates[] | select(.activity_name == "litellm_chat")'
# List environment variable keys for a collection
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/collections/{COLLECTION}/environment" | jq '.environment_variables | keys'
# Set an environment variable (merge semantics — other vars preserved)
# Use stdin to avoid exposing the value in process args
cat <<'BODY' | curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/api/v1/collections/{COLLECTION}/environment" \
--data-binary @-
{"environment_variables": {"KEY_NAME": "value"}}
BODY
# Remove an environment variable (pass null to delete)
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/api/v1/collections/{COLLECTION}/environment" \
-d '{"environment_variables": {"KEY_NAME": null}}'
# Check which secrets a runbook needs vs what's configured
# 1. Parse the runbook's frontmatter secrets block
# 2. GET the collection's environment variable keys
# 3. Compare and report missing
When deploying a runbook as a Jetty task:
secrets blocksecrets.*.envinit_params in the run requestThe run request supports secret_params for ad-hoc secret passing:
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-F 'init_params={"prompt": "analyze this"}' \
-F 'secret_params={"TEMP_API_KEY": "sk-..."}' \
"https://flows-api.jetty.io/api/v1/run/{COLLECTION}/{TASK}"
secret_params are merged into the runtime environment (same as collection env vars) but are NEVER stored in the trajectory. Use this for one-off runs; for production, configure secrets as collection environment variables.
A runbook is a structured markdown document (RUNBOOK.md) that tells a coding agent how to accomplish a complex, multi-step task with evaluation loops and quality gates. Runbooks can be executed locally (the agent follows the runbook directly) or remotely on Jetty (via the chat-completions endpoint).
When the user says "run runbook", determine the mode:
If ambiguous, use AskUserQuestion to ask.
The agent becomes the executor. Read the RUNBOOK.md and follow it step by step.
version, evaluation pattern, and secretsecho "${SECRET_NAME:+SET}". If missing, prompt the user.mkdir -p {{results_dir}}{{results_dir}} (defaults to ./results locally)# Example: user says "run the runbook with sample_size=5"
mkdir -p ./results
# Then follow each step from the RUNBOOK.md...
Launch the runbook on Jetty's sandboxed infrastructure via the OpenAI-compatible chat-completions endpoint.
Endpoint: POST https://flows-api.jetty.io/v1/chat/completions
agent, model, model_provider, snapshot, and secrets:
agent → use as jetty.agent (default: claude-code)model → use as model in the request (default: claude-sonnet-4-6)model_provider → use as jetty.model_provider (default: anthropic)snapshot → use as jetty.snapshot (default: python312-uv; use prism-playwright if the runbook needs a browser)secrets → check that each required secret is configured as a collection env var:curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/collections/{COLLECTION}/environment" | jq '.environment_variables | keys'
If any required secrets are missing, prompt the user to set them (or pass via secret_params).{{template_variable}} placeholders and their defaults. Ask the user for any required parameter values that are missing (use AskUserQuestion). These go in jetty.template_variables.use_trial_keys: true in the jetty block below.system message, and template variables go in jetty.template_variables (not in the user message):# Read the runbook content
RUNBOOK_CONTENT="$(cat /path/to/RUNBOOK.md)"
# Check trial eligibility (see Trial Key Support section)
TOKEN="$(cat ~/.config/jetty/token)"
TRIAL=$(curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/trial/{COLLECTION}")
TRIAL_ACTIVE=$(echo "$TRIAL" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('active', False))")
COLL=$(curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/collections/{COLLECTION}/environment")
HAS_KEYS=$(echo "$COLL" | python3 -c "
import sys, json
d = json.load(sys.stdin)
evars = d.get('environment_variables', {})
keys = ['OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'REPLICATE_API_TOKEN']
print(any(k in evars for k in keys))
")
# Set USE_TRIAL to true if trial is active and no provider keys exist
USE_TRIAL=$( [ "$TRIAL_ACTIVE" = "True" ] && [ "$HAS_KEYS" = "False" ] && echo true || echo false )
# Build the request payload
cat <<PAYLOAD | curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/v1/chat/completions" \
--data-binary @-
{
"model": "claude-sonnet-4-6",
"messages": [
{"role": "system", "content": $(jq -Rs '.' <<< "$RUNBOOK_CONTENT")},
{"role": "user", "content": "Execute the runbook."}
],
"stream": false,
"jetty": {
"runbook": true,
"collection": "{COLLECTION}",
"task": "{TASK}",
"agent": "claude-code",
"model_provider": "anthropic",
"snapshot": "python312-uv",
"template_variables": {
"sample_size": "10",
"results_dir": "/app/results"
},
"use_trial_keys": $USE_TRIAL
}
}
PAYLOAD
Important: Template variables ({{sample_size}}, {{results_dir}}, etc.) must go in jetty.template_variables, NOT in the user message text. The backend substitutes {{var}} placeholders in the runbook instruction before the agent sees it. The user message is only used as a prompt/instruction to the agent.
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/db/trajectory/{COLLECTION}/{TASK}/{TRAJECTORY_ID}" | jq '{status, steps: (.steps | keys)}'
The chat-completions endpoint supports two modes via a single URL:
| Mode | Trigger | Behavior |
|---|---|---|
| Passthrough | No jetty block | OpenAI-compatible LLM proxy — streams tokens from 100+ providers |
| Runbook | jetty block present | Full agent execution in an isolated sandbox |
Jetty block fields:
| Field | Type | Required | Description |
|---|---|---|---|
jetty.runbook | boolean | Yes | Enable runbook/agent mode |
jetty.collection | string | Yes | Namespace for the task |
jetty.task | string | Yes | Task identifier |
jetty.agent | string | Yes | claude-code (recommended default), opencode, codex, or gemini-cli |
jetty.model_provider | string | No | anthropic, openrouter, openai, google, bedrock. Set explicitly to avoid relying on inference |
jetty.snapshot | string | No | Sandbox snapshot: python312-uv (default) or prism-playwright (browser). Read from runbook frontmatter |
jetty.template_variables | object | No | Key-value pairs for {{var}} substitution in the runbook instruction. results_dir defaults to /app/results |
jetty.file_paths | string[] | No | Files to upload into the sandbox |
jetty.use_trial_keys | boolean | No | Use Jetty trial keys (default: false). Set to true for trial users with no own provider keys |
File upload (if the runbook needs input files):
# Upload a file first
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: multipart/form-data" \
-F "file=@/path/to/input.csv" \
-F "collection={COLLECTION}" \
"https://flows-api.jetty.io/api/v1/files/upload" | jq
# Then reference the returned path in file_paths
With the OpenAI Python SDK:
from openai import OpenAI
client = OpenAI(
base_url="https://flows-api.jetty.io",
api_key="your-jetty-api-token"
)
# Read runbook
with open("RUNBOOK.md") as f:
runbook = f.read()
response = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[
{"role": "system", "content": runbook},
{"role": "user", "content": "Execute the runbook."}
],
stream=True,
extra_body={
"jetty": {
"runbook": True,
"collection": "my-org",
"task": "my-task",
"agent": "claude-code",
"model_provider": "anthropic",
"snapshot": "python312-uv", # or "prism-playwright" for browser
"template_variables": {
"sample_size": "10",
},
}
}
)
Sandbox conventions:
{{results_dir}}, {{sample_size}}, etc.) are substituted by the backend before the agent sees the instruction. Pass them in jetty.template_variables, never in the user message textresults_dir defaults to /app/results on Jetty (vs ./results locally) — it's auto-included as a template variable/app/results/ is persisted to cloud storagesnapshot controls the sandbox image: python312-uv (default) or prism-playwright (Playwright + Chromium for browser tasks). Read this from the runbook's YAML frontmatterA routine is a saved schedule that fires an existing task on a recurring cadence. Routines build on the same FlowWorkflow.run pipeline as one-shot runs, with optional init_params_overrides merged on top of the task's defaults. Trajectories produced by a routine are tagged with triggered_by_routine_id for easy filtering.
Use the MCP tools (preferred) or hit the REST API directly:
| Tool | Endpoint |
|---|---|
list-routines | GET /api/v1/routines/{COLLECTION} or GET /api/v1/routines/{COLLECTION}/{TASK} |
get-routine | GET /api/v1/routines/{COLLECTION}/{TASK}/{NAME} |
create-routine | POST /api/v1/routines/{COLLECTION}/{TASK} |
update-routine | PATCH /api/v1/routines/{COLLECTION}/{TASK}/{NAME} |
delete-routine | DELETE /api/v1/routines/{COLLECTION}/{TASK}/{NAME} |
pause-routine / resume-routine | POST .../pause / POST .../resume |
run-routine-now | POST .../run-now — returns workflow_id |
list-routine-runs | GET .../runs — recent trajectories |
Cadence enum (UTC only in v1):
cadence.type | Required fields | Behavior |
|---|---|---|
manual | — | Saved invocation preset; only run-routine-now triggers it. No Temporal schedule registered. |
hourly | minute_utc (default 0) | Fires every hour at minute_utc. |
daily | hour_utc, minute_utc? | Fires once per day at the given UTC time. |
weekdays | hour_utc, minute_utc? | Fires Mon–Fri only at the given UTC time. |
weekly | day_of_week, hour_utc, minute_utc? | Fires once per week. |
Validation rules (enforced server-side):
init_params_overrides keys MUST be a subset of task.workflow.init_params. Unknown keys return 400 with the offending key list.daily / weekdays / weekly require hour_utc. weekly additionally requires day_of_week.manual rejects cadence params other than type.Example: schedule a daily 9am UTC summary
TOKEN="$(cat ~/.config/jetty/token)"
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/api/v1/routines/{COLLECTION}/{TASK}" \
-d '{
"name": "daily-summary",
"cadence": {"type": "daily", "hour_utc": 9, "minute_utc": 0},
"init_params_overrides": {"prompt": "Summarize yesterday"}
}' | jq
To inspect runs from a routine, call list-routine-runs (or hit .../runs) — these are the same trajectories you would see via list-trajectories, filtered to those tagged with triggered_by_routine_id.
# List datasets
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/datasets/{COLLECTION}" | jq
# List models
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/models/{COLLECTION}/" | jq
A Jetty workflow is a JSON document with three sections:
{
"init_params": { "param1": "default_value" },
"step_configs": {
"step_name": {
"activity": "activity_name",
"param1": "static_value",
"param2_path": "init_params.param2"
}
},
"steps": ["step_name"]
}
| Component | Description |
|---|---|
init_params | Default input parameters |
step_configs | Configuration per step, keyed by step name |
steps | Ordered list of step names to execute |
activity | The step template to use |
*_path suffix | Dynamic reference to data from init_params or previous steps |
init_params.prompt # Input parameter
step1.outputs.text # Output from step1
step1.outputs.items[0].name # Array index access
step1.outputs.items[*].id # Wildcard (returns array of all ids)
step1.inputs.prompt # Input that was passed to step1
For workflow templates (simple chat, image generation, model comparison, fan-out, etc.), read references/workflow-templates.md.
The step template docs and actual runtime parameters differ for several activities. These mismatches cause silent failures — always use the runtime names below.
litellm_chatprompt / prompt_path (NOT user_prompt / user_prompt_path)system_prompt / system_prompt_path works as documentedprompt_path must resolve to a single string. If it resolves to an array
(e.g. init_params.distill_texts: ["a", "b", "c"]), the Anthropic provider
path crashes deep in litellm with
TypeError: string indices must be integers, not 'str' (the is_pdf_used
check assumes each content entry is a dict and indexes it with "type").
Workaround: stringify the array caller-side and pass a single string
(prompt: JSON.stringify(arr) or a delimited join), and describe the shape
in system_prompt so the model knows to parse it."I understand. I'm ready to receive..." style messages
instead of the expected output, the user prompt is reading as context
rather than as a request. Two patches that consistently fix this in
practice:
system_prompt, explicitly bracket the input
("The subject input begins below the horizontal rule and ends at end-of-message.") and name the deliverable."\n\nProduce the X JSON now per your output schema. Begin your response with { and end with }. No preamble, no markdown fences, no commentary." Together these make the request shape
unambiguous even on Haiku-tier models.list_emit_awaitchild_workflow_name (for example, process-single-item), not
{COLLECTION}/process-single-item.child_init_params / child_init_params_path for values shared by every
emitted child workflow..outputs.trajectory_references (NOT .outputs.trajectory_ids).items: [...] (literal array) is not accepted — only items_path: "<ref>".
Passing a literal items array fails workflow start with ValueError: items_path is required. Workaround: hoist the literal set into init_params
and reference it, e.g. init_params.perspectives: ["a", "b", "c", "d"] plus
items_path: "init_params.perspectives".extract_from_trajectoriestrajectory_list_path pointing at a list of trajectory reference objects
(usually fanout_step.outputs.trajectory_references), NOT
trajectory_ids_path.extract_keys as a dictionary of output key to child trajectory path, NOT
extract_paths.{
"activity": "extract_from_trajectories",
"trajectory_list_path": "process_all.outputs.trajectory_references",
"extract_keys": {
"text": "extract.outputs.text"
}
}
replicate_text2image.outputs.images[0].path (NOT .outputs.storage_path or .outputs.image_url).outputs.images[0].extension, .outputs.images[0].content_typegemini_image_generator.outputs.images[0].path (NOT .outputs.storage_path)litellm_visionimage_path_expr (NOT image_url_path)image_url_path is for external HTTP URLs onlyflows-api.jetty.io: passing an external URL via image_url/image_url_path fails with ValueError: image_path is required for vision models, and passing the same URL to image_path/image_path_expr makes Jetty parse https://... as a workflow reference (task_name/trajectory_id) and fail with ValueError: Invalid workflow reference: https. In that case litellm_vision only accepted an internal collection-prefixed storage key via image_path. If your image lives on an external CDN, first upload it to Jetty storage (POST /api/v1/files) and reference the returned storage key — or call the vision model's provider SDK directly outside Jetty.simple_judgeitem / item_path (NOT content / content_path)instruction / instruction_path (NOT criteria / criteria_path)items / items_path.webp/.png/.jpg storage path as item_pathscore_range in categorical mode uses range values as labels, not numeric scores# 1. Find the failed trajectory
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/db/trajectories/{COLLECTION}/{TASK}?limit=5" \
| jq '.trajectories[] | {trajectory_id, status, error}'
# 2. Examine which step failed (steps is an object, not array)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/db/trajectory/{COLLECTION}/{TASK}/{TRAJECTORY_ID}" \
| jq '.steps | to_entries[] | select(.value.status == "failed") | {step: .key, error: .value}'
# 3. Check workflow logs
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/workflows-logs/{WORKFLOW_ID}" | jq
Errors from fan-out children often surface on the parent. When a
list_emit_await orchestrator reports something vague like "synthesize step
missing", the real failure is usually in one of the child trajectories — for
example, a downstream LLM call rejecting an empty prompt because the parent
didn't forward init_params correctly. Walk the chain:
# 1. Pull the parent trajectory and find the fan-out step.
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/db/trajectory/{COLLECTION}/{ORCHESTRATOR_TASK}/{TRAJECTORY_ID}" \
| jq '.steps.{FANOUT_STEP}.outputs.trajectory_references[] | {trajectory_id, name}'
# 2. For each child trajectory, fetch and look at its failed step.
CHILD_ID=... # from step 1
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/db/trajectory/{COLLECTION}/{CHILD_TASK}/${CHILD_ID}" \
| jq '{status, error, steps: (.steps | to_entries[] | select(.value.status == "failed") | {step: .key, error: .value.error})}'
The root cause is almost always at the deepest layer (the LLM call's error
message), not the parent's "missing step" summary. If a child trajectory's
init_params arrive empty or wrong-shaped, that's a signal the orchestrator's
state wiring (e.g. child_init_params / child_init_params_path) isn't
forwarding what you expected — re-check the parent's step config.
# 1. Create
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://flows-api.jetty.io/api/v1/tasks/{COLLECTION}" \
-d '{
"name": "test-echo",
"description": "Simple echo test",
"workflow": {
"init_params": {"text": "Hello!"},
"step_configs": {"echo": {"activity": "text_echo", "text_path": "init_params.text"}},
"steps": ["echo"]
}
}' | jq
# 2. Run sync
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-F 'init_params={"text": "Test message"}' \
"https://flows-api.jetty.io/api/v1/run-sync/{COLLECTION}/test-echo" | jq
# 3. Check result
curl -s -H "Authorization: Bearer $TOKEN" \
"https://flows-api.jetty.io/api/v1/db/trajectories/{COLLECTION}/test-echo?limit=1" | jq '.trajectories[0]'
For batch run scripts, read references/batch-runs.md.
| Status | Meaning | Resolution |
|---|---|---|
| 401 | Invalid/expired token | Regenerate at jetty.io → Settings → API Tokens |
| 403 | Access denied | Verify token has access to the collection |
| 404 | Not found | Check collection/task names for typos |
| 422 | Validation error | Check request body format and required fields |
| 429 | Rate limited | Reduce request frequency, implement backoff |
| 500 | Server error | Retry with exponential backoff |
TOKEN="$(cat ~/.config/jetty/token)" at the start of each bash block — env vars don't persist across invocationsjq -r '.field' to extract without quotes; jq '.trajectories[0]' for first resultinit_params for a trajectory are at .init_params.prompt, not .steps.{step}.inputs.promptjq '.events[] | select(.level == "error")'curl -v for debugging request/response issuesnpx claudepluginhub jettyio/jettyio-skills --plugin jettyGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.