Custom Integrations
Jetty has no fixed list of supported services. An integration is a credential you store on your collection and a runbook that declares it, so anything with an API, a CLI or an MCP server can be connected the same way. This guide is the general recipe behind the GitHub, Google Drive and Slack guides; use it for a service that has no guide of its own, including your own application.
The worked example connects a runbook to an internal orders API with two variables, APP_BASE_URL and APP_API_TOKEN. Swap in your service's names and endpoints.
Prerequisites:
- A way to mint a credential for the service: an API token, a webhook URL or a connection string
- A Jetty collection where you can set environment variables and create tasks
How it works
Every integration has the same four parts:
| Part | Where it lives | What it does |
|---|---|---|
| The credential | The service | Says what the runbook may do there. Mint one for Jetty, with the smallest scope that does the job. |
| The variable | Your collection's environment | Holds the value under a name, such as APP_API_TOKEN. |
| The declaration | The runbook's secrets: frontmatter | Names the variables this runbook needs. Only declared variables reach the run's agent. |
| The instructions | The runbook body | Tell the agent which variable to use for which call, and to check it before doing any work. |
For what happens between the collection and the sandbox, see Secrets and Environment Variables.
Set up the integration
Step 1: Mint a credential for the runbook
Create a credential that belongs to the runbook, not to a person. A few rules hold for almost every service:
- Smallest scope. Read-only if the runbook only reads; one project, repository or channel if the service can scope that narrowly.
- Long-lived, not interactive. A run has no browser and nobody to click through a login. Use an API token, a service account key, a webhook URL or a connection string. For an OAuth-only service, mint a refresh token once and store it with the client id and secret, as the Google Drive guide does.
- Revocable on its own. A dedicated token can be rotated or revoked without touching anything else.
Step 2: Choose the variable names
Use upper-case names with underscores, prefixed with the service: APP_API_TOKEN, HUBSPOT_TOKEN, DATABASE_URL. Store non-secret configuration (a base URL, a workspace id) as its own variable beside the token, so the runbook never hard-codes an environment.
Note
Names ending in _TOKEN, _API_KEY, _API_TOKEN, _API_SECRET, _SECRET_KEY or _ACCESS_KEY are recognised as secrets wherever Jetty records a step's configuration, and their values are replaced with [REDACTED]. Prefer one of those endings for anything sensitive.
Step 3: Store the values on your collection
There are three ways to set a collection variable. All three write to the same place.
Web app. Open Settings → Integrations (/settings/integrations) and add each name and value.
Your coding agent. With the Jetty plugin or MCP server installed, ask the agent to store it. Keep the value in a file so it doesn't land in the chat:
Read the token in ~/orders-token.txt and store it as APP_API_TOKEN on my <your-collection> collection. Also set APP_BASE_URL to https://orders.example.com.API. Send a PATCH to the collection's environment endpoint. The body is wrapped in environment_variables, and the call merges these keys into the variables already there:
curl -X PATCH "https://flows-api.jetty.io/api/v1/collections/<your-collection>/environment" \
-H "Authorization: Bearer $JETTY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"environment_variables": {"APP_BASE_URL": "https://orders.example.com", "APP_API_TOKEN": "<token>"}}'To remove a variable, send its name with null:
curl -X PATCH "https://flows-api.jetty.io/api/v1/collections/<your-collection>/environment" \
-H "Authorization: Bearer $JETTY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"environment_variables": {"APP_API_TOKEN": null}}'Step 4: Declare the secrets in the runbook
Important
A collection variable is not visible to a runbook until the runbook declares it. The agent receives the variables listed in the secrets: block of the frontmatter, plus the model provider's own key. Without the declaration the sandbox has no APP_API_TOKEN.
---
name: orders-digest
version: 1.0.0
description: Fetch yesterday's orders from the orders API and write a one-page digest
secrets:
APP_BASE_URL: { env: APP_BASE_URL }
APP_API_TOKEN: { env: APP_API_TOKEN }
---Each entry is a name and, under env:, the environment variable the run delivers the value in. The frontmatter never holds a value. A declared secret the collection hasn't set does not stop the run: Jetty writes a line to the run log and the agent starts without it, which is why the runbook checks for it first (Step 5).
If you work through the Jetty plugin or MCP server, its check-secrets tool compares these names with what your collection has set and returns {configured, missing, ready}, names only.
Step 5: Write the runbook around the credential
Three habits make an integration dependable:
- Preflight. Check that each variable is set before doing any work, and stop with a clear message if not.
- Prove the credential. Make one cheap authenticated call (a
whoami, a ping,SELECT 1) so a bad token fails in seconds. - Keep the value out of the output. Tell the agent never to print the token or put it in a URL. Jetty records what the agent prints and writes, exactly as produced.
# Orders Digest
You fetch yesterday's orders from the orders API and write a one-page digest.
The API's base URL is in the environment as `APP_BASE_URL` and its token as `APP_API_TOKEN`.
Never print, echo, or write the token to any file or log, never put it in a URL, and never run
`env` or `printenv` without filtering it out.
## Step 1 — Preflight
```bash
mkdir -p /app/results
[ -n "${APP_BASE_URL:-}" ] || echo "MISSING APP_BASE_URL"
[ -n "${APP_API_TOKEN:-}" ] || echo "MISSING APP_API_TOKEN"
```
If either is missing, write `/app/results/summary.md` saying which, and stop.
## Step 2 — Check the credential
```bash
curl -sS -o /app/results/whoami.json -w '%{http_code}\n' \
-H "Authorization: Bearer $APP_API_TOKEN" "$APP_BASE_URL/v1/whoami"
```
Anything other than `200` is a `FAIL`: record the status code (never the token) in
`/app/results/summary.md` and stop.
## Step 3 — Fetch the orders
```bash
curl -sS -G -H "Authorization: Bearer $APP_API_TOKEN" "$APP_BASE_URL/v1/orders" \
--data-urlencode "date={{date}}" > /app/results/orders.json
```
## Step 4 — Summary
Write `/app/results/summary.md`: the order count, the total, the three largest orders, and
`PASS` or `FAIL` with the failing call and its status code.Connecting through an MCP server
If the service has a remote MCP server, the runbook can attach it and let the agent use its tools. Name the secret that authenticates the server with secret::
---
name: issue-triage
version: 1.0.0
description: Triage new issues through the tracker's MCP server
agent: claude-code
mcp_servers:
tracker:
url: https://mcp.tracker.example.com/mcp
secret: TRACKER_TOKEN
secrets:
TRACKER_TOKEN: { env: TRACKER_TOKEN }
---At run time Jetty reads TRACKER_TOKEN from the run's environment and writes it as the server's Authorization: Bearer header in the agent's MCP configuration. Because Jetty consumes the secret, it is not also placed in the agent's shell environment. If the runbook body needs the same token for a direct API call, opt in:
secrets:
TRACKER_TOKEN: { env: TRACKER_TOKEN, expose_to_agent: true }A server definition can't carry a credential itself: an Authorization header, or any header or env key that looks like a token, is dropped with a warning, because the definition is stored with the task. Use secret:. MCP support varies by runtime; see Runtime Configuration.
Overriding a credential for one run
To point one run at a different account or environment, pass secret_params with the run request. They are merged over the collection's variables for that run only and are not stored with the run:
curl -X POST "https://flows-api.jetty.io/api/v1/run/<your-collection>/orders-digest" \
-H "Authorization: Bearer $JETTY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"init_params": {"vars": {"date": "2026-11-09"}},
"secret_params": {"APP_BASE_URL": "https://orders.staging.example.com", "APP_API_TOKEN": "<staging token>"}
}'A schedule can carry its own secret_params the same way; the API never returns them once set.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Preflight reports MISSING APP_API_TOKEN | The variable isn't in the runbook's secrets: block, or isn't set on the collection | Add the declaration (Step 4) and check the collection (Step 3). The run log names a declared secret the run did not set |
| The variable is set and declared, but still empty in the shell | An MCP server or a code-check source names the same secret with secret:, so Jetty withholds it from the agent | Add expose_to_agent: true to the entry |
The service answers 401 or 403 | The token is wrong, expired, or lacks a scope | Test the token from your own machine with the same call, then update the collection value; the next run picks it up |
| The declaration was added but the run behaves as before | The task still holds the earlier runbook text | Save the runbook to the task again, then rerun |
| The MCP server's tools never appear | The chosen runtime doesn't load MCP servers, or the token was missing | Pick a runtime with MCP support and check the run log for the server's token line |
Security checklist
- One credential per integration, minted for Jetty, with the smallest scope that works.
- Keep the value only in the collection environment. Never put it in the runbook text, the task's
varsor a URL. - Anyone who can read the collection's settings can read its variables, and any runbook in the collection can declare them. Put credentials with different audiences in different collections.
- Keep the “never print the token” line in the runbook.
- Rotate by updating the collection value; revoke at the service to cut off a run in progress.