Build an Agenta agent
You turn a plain-language request into a working, tested agent on Agenta. You do it by
writing one JSON config and running a few bundled scripts. Optimize for the fewest calls
and the least time. The simplest agent is two steps: write the config, run build.sh.
Do not over-think simple asks.
Agenta is an open-source platform for building, testing, and deploying LLM apps and agents.
This skill builds Agenta agents over the Agenta HTTP API.
Use this skill first. Everything you need is in this folder: the playbook below and the
scripts in scripts/. These facts were confirmed against a live Agenta deployment. Do not
browse the web or the codebase by default. Only if you get truly stuck do the open-source
repo (https://github.com/Agenta-AI/agenta) and docs (https://docs.agenta.ai) exist as a
fallback; prefer what is written here.
First run: credentials and prerequisites
Do these two things once, before any build.
1. Credentials. The scripts need AGENTA_API_KEY, and AGENTA_HOST only if the user
self-hosts (it defaults to Agenta cloud, https://cloud.agenta.ai).
- Ask the user for their API key. Point them to it: the API keys page in their Agenta
project settings (on cloud, under
https://cloud.agenta.ai). Ask for the host URL only if
they self-host, in which case it is their own Agenta domain.
- Then offer to set it up for them, either way they prefer:
- Write it for them: create a
.env file in the working directory with
AGENTA_API_KEY=... (and AGENTA_HOST=... if self-hosting). lib.sh picks up a local
.env automatically.
- Hand them a block to paste: give them the lines to
export, or the .env contents
to create themselves.
- Never print or commit the key. Add
.env to .gitignore if the directory is a repo.
2. Prerequisites. Run bash scripts/check-prereqs.sh. It verifies bash, curl, and
jq are installed. On a miss it prints the exact install command for the user's platform;
show that to the user and ask them to install the tool, then continue.
The shape of every agent
An agent is one JSON object, parameters.agent. You only ever decide four things inside it:
instructions.agents_md (who it is / what it does), tools (integration actions it can
call), skills (reusable know-how), and whether it needs a trigger (a schedule or an event).
The rest is fixed boilerplate — copy it exactly:
{
"instructions": { "agents_md": "<what the agent is and does>" },
"llm": { "model": "sonnet", "provider": "anthropic", "connection": { "mode": "self_managed" } },
"tools": [],
"mcps": [],
"skills": [],
"harness": { "kind": "claude" },
"runner": { "kind": "sidecar", "interactions": { "headless": "auto" } },
"sandbox": { "kind": "local" }
}
Keep llm / harness / runner / sandbox exactly as above unless told otherwise. model
is an alias (sonnet, opus, haiku, default), never a raw model id. For the full config
field by field — tool entries, skill entries, and the fields that 500 if misplaced — read
references/config-schema.md.
The loop (run it in order, skip what the ask doesn't need)
- Read the ask. Decide what it needs using the decision table below. Most asks need only
instructions. Do not discover tools or triggers an ask does not call for.
- If it needs integration actions, discover them with
bash scripts/discover-tools.sh "<capability>". This prints the exact tools entries and each connection's state. Detail:
references/tools-and-connections.md.
- If a needed connection is not
ready, STOP and ask the user to connect it. Do not
guess, do not fabricate a result. Report which integration needs connecting and why, then
stop. That is a correct, complete outcome, not a failure.
- Write the config to a file under
configs/. Integration actions go in tools,
reusable know-how in skills, the behavior in instructions.agents_md. For a multi-tool
or scheduled agent, write instructions.agents_md as an explicit numbered procedure that
names the exact tools in order and ends on the terminal action — otherwise the run wanders
or stops short. Depth in references/writing-instructions.md.
- Create and test in one shot:
bash scripts/build.sh <config> <slug> "<test message>".
Read the OUTPUT and RESOLVED lines. RESOLVED must show
harness=claude model=sonnet connection=self_managed.
- If it needs a trigger, create it against the variant id AND the revision id (both
printed by
create-agent.sh / build.sh) — a schedule with bash scripts/create-schedule.sh,
an event subscription with bash scripts/create-subscription.sh (only after
discover-triggers.sh and a ready connection). Passing the revision id is required: without
it the trigger has no bound version to run, which the Agenta UI treats as an error. Pass the
literal word latest in place of a revision id if you only have the variant id at hand.
Confirm with bash scripts/triggers.sh schedules (or subscriptions).
- Report in a short bullet list: what you built, the artifact ids, what you tested, the
result, and anything that needs the user.
Decision table
| The ask… |
Needs |
What to add |
| transform text the user pastes (summarize, rewrite, classify) |
nothing extra |
instructions only |
| apply a body of know-how (a writing style, a review rubric) |
a skill |
one skills entry |
| read or write in an outside tool (GitHub, Slack, …) |
gateway tools |
discover-tools.sh, then tools entries |
| run on a clock (e.g. twice a day) |
a schedule |
build the agent, then create-schedule.sh |
| react to an outside event (new issue, new message) |
a subscription |
discover-triggers.sh, then create-subscription.sh (needs a ready connection) |
| change an agent that already exists |
a new revision |
update-agent.sh against the variant id, then re-test |
Scripts (your only interface to the API)
Run them as bash scripts/<name> from this skill's directory. They wrap real Agenta
endpoints and load credentials themselves — you never handle the API key.
check-prereqs.sh — verify bash/curl/jq before you start.
build.sh <config.json> <slug> "<msg>" [name] — create the agent AND test it. The fast path.
create-agent.sh <config.json> <slug> [name] — create only; prints application_id /
variant_id / revision_id. Use when you need the variant id before scheduling.
test-agent.sh <application_id> <config.json> "<msg>" — batch invoke + verify. A single
non-streaming call returns the full real turn, so it prints OUTPUT (the last assistant
message), a TOOLS: line (every tool called, in order, with call/result counts), an
APPROVAL: line when the run paused on an approval gate, RESOLVED (the config the run
actually used), and the TRACE id.
discover-tools.sh "<capability>" ... — find gateway tools for plain-language actions; shows
each connection's state and the ready-to-wire tool object.
discover-triggers.sh "<event>" ... — find event triggers (for subscriptions only).
update-agent.sh <variant_id> <config.json> [message] — commit a new config to an EXISTING
agent. Prefer this over archive-and-recreate: the slug, the ids, and any triggers survive.
create-schedule.sh <variant_id> <revision_id> "<cron UTC>" <event_key> [name] [inputs_json]
— cron trigger. The revision id (from create-agent.sh / build.sh) pins which committed
version runs; omitting it leaves the schedule with no bound revision. Pass latest instead
of a real id to have the script look up the variant's current HEAD revision for you.
create-subscription.sh <variant_id> <revision_id> <event_key> <connection> [name] [trigger_config_json] [inputs_json] — event trigger. Same revision-id requirement (and
latest shortcut) as create-schedule.sh. The connection (id or slug) must be ready
first; verify actual fires with triggers.sh deliveries.
triggers.sh schedules|subscriptions|deliveries|rm-schedule <id>|rm-subscription <id>.
check-tools.sh <trace_id> [terminal_tool] — OPTIONAL fallback. test-agent.sh already lists
the tools a run called (its TOOLS: line). Reach for this only to confirm a gated WRITE
actually returned ok:true — pass the terminal tool for a PASS/INCOMPLETE verdict from the
trace spans.
archive-agent.sh <application_id> — soft-delete an app; use it to clean up or free a slug.
Two extras live in scripts/extras/ and stay out of the loop: list-connections.sh
(discover already prints the CONNECTIONS block; see references/tools-and-connections.md)
and annotate-trace.sh (an experimenter demo; see references/annotate-trace.md).
Deeper topics (read on demand)
Keep to the loop above for a simple agent. Read one of these only when the task calls for it.
| To do this |
Read |
| Understand the full agent config field by field |
references/config-schema.md |
| Discover tools, wire a connection, add an event trigger |
references/tools-and-connections.md |
| Write instructions for a multi-tool or scheduled agent |
references/writing-instructions.md |
| Shape the inputs a schedule or subscription passes to the run |
references/trigger-inputs.md |
| Record a reflection against a trace |
references/annotate-trace.md |
Hard rules (these prevent the usual failures)
- A trigger needs the revision id, not just the variant id.
create-schedule.sh and
create-subscription.sh both take <variant_id> <revision_id> ... — pass both (printed by
create-agent.sh / build.sh), or pass latest in place of the revision id if you only have
the variant id at hand. Omitting the revision id creates a trigger with no bound version,
which the Agenta UI treats as an error ("Which version runs?" required, unset).
- Fewest calls. A no-tools agent is exactly two actions: write the config, run
build.sh.
Don't re-list, don't re-verify facts already given here, don't re-read the schema.
- Test before you declare done. A claim with no
build.sh / test-agent.sh behind it does
not count. Check OUTPUT is right and RESOLVED shows the intended config.
- Write the persona as an explicit imperative. The agent is Claude Code underneath; on
ambiguous input it falls back to a generic coding-assistant persona instead of doing the job.
Author
instructions.agents_md as an explicit imperative — who the agent is and what it does,
stated as a command, not a vague topic. (Likewise phrase a one-off test with a verb:
"Summarize the following text:\n\n<text>", not a bare <text>, so the test isn't read as a
chat opener.)
- Crons are UTC, five fields, one-minute floor. Convert the user's local time yourself.
- needs_auth means stop. If a needed connection is not
ready, stop and ask the user to
connect it. That is the whole job for that step.
- Discovery is a search, not an oracle.
discover-tools.sh / discover-triggers.sh return
high-recall best guesses over the live catalog. The matched connection state is reliable;
the matched integration and action are not always right. A search matches on words: "save
notes to Notion" can match Slack's SEARCH_MESSAGES as the primary tool and still print
READY: true — while the Notion action you wanted is buried in alternatives and its
connection is needs_auth. So: confirm the matched tool's integration is the one you
asked for, and treat the per-integration CONNECTIONS: block (not the headline READY
line) as the source of truth. If the match looks off, re-word the fragment or pick from the
alternatives — a search for "new telegram message" can return a Slack event the same way.
For discover-triggers.sh specifically, a right integration is not enough — the keyword
match can land on the wrong event within that integration (e.g. "new github issue opened"
matching a GITHUB_ARTIFACT_CREATED_TRIGGER event purely on the shared word "created", while
the connection still reports ready). Always read the fires_on line the script prints for
the MATCHED event, and the ALTERNATIVES block, before wiring anything into
create-subscription.sh — a plausible-looking event_key with the right integration and a
ready connection is not proof it fires on what you asked for. If nothing in MATCHED or
ALTERNATIVES actually corresponds to the requested event, stop and tell the user this
integration doesn't support that trigger yet — do not wire up the closest keyword hit.
- Change an existing agent by committing, not recreating.
update-agent.sh <variant_id> <config.json> commits a new revision to the same app; the slug, the ids, and any schedules
or subscriptions keep working. Archive-and-recreate loses all of them. Re-test after the
update with test-agent.sh.
- A failed build still creates the app. If
build.sh creates the app but the invoke fails,
the app (and its slug) already exist. Fix the config and re-test with test-agent.sh against
the existing application_id — do NOT re-run build.sh with the same slug (it conflicts).
For a clean retry, archive-agent.sh <application_id> first.
- Invoke carries the config inline (a lab artifact, not a gap).
build.sh and
test-agent.sh inline the full config on every invoke against the low-level agent-service
endpoint, which does not hydrate a committed reference. That part is unrelated to the
endpoint/auth/body-shape contract itself, which is settled — see
docs/designs/invoke-negotiations/specs.md in the agenta repo. The product API invoke path
loads a committed reference server-side, so bare-reference invoke is a real product feature;
this kit still doesn't use that path, and there's no need to rewrite the scripts to invoke by
bare reference.
- Report short. A few bullets. Lead with the result and the artifact ids.
Verify (don't trust a 200)
test-agent.sh sends a non-streaming invoke (Accept: application/json). The response's
data.outputs carries the run's full real turn, not just a final string: messages (assistant
text plus a role: "tool" entry for every tool call and result, in order), stop_reason, and —
only when the run paused on an approval gate — pending_interaction. The script reads this
directly, so OUTPUT / TOOLS / APPROVAL below come straight off the invoke response, no trace
needed. (Endpoint, auth, body shape, and this response contract are settled — see
docs/designs/invoke-negotiations/specs.md in the agenta repo. The script sends no extra
headers, so it gets the defaults: full transcript, resolved workflow embeds.)
test-agent.sh also prints RESOLVED from the run's trace. It must match what you intended. If
it shows a different harness/model, the run silently fell back — fix the config and re-test.
Traces flush a second or two late; the script already retries.
Multi-tool agents — read the TOOLS: line, not just OUTPUT. A run that makes several tool
calls can end on its tool-call turn, so OUTPUT (the last assistant message) is genuinely EMPTY —
that is not a failure or a parsing gap, it means the run's last turn was a tool call rather than
a reply. test-agent.sh's TOOLS: line lists every tool the run CALLED, in order, with each
result's ok/ERROR (e.g. github__LIST_COMMITS -> ok -> slack__SEND_MESSAGE -> ok (2 calls, 2 results)). That line is your reliable "did it reach the terminal tool, and did that tool return
ok" signal. If the terminal tool is in the list with an ok result, the run got there and
finished; if it's missing, the run STOPPED SHORT (did the early reads, then wandered and never
reached the final action). If it stopped short, re-test with a blunter, numbered instruction —
the config is usually fine, the run just wandered. See references/writing-instructions.md.
test-agent.sh prints an APPROVAL: line when stop_reason == "paused" — a WRITE (like
SEND_MESSAGE) tripped a human-approval gate. The run stopped there: that tool's call is in
TOOLS, but it has no matching result yet, because the run paused before executing it. The write
may not complete within this invoke at all (the approval can go unresolved).
check-tools.sh is the fallback for what this invoke can't show yet: whether a gated tool
actually RETURNED after the fact. It reads the trace spans — a tool's span appears as soon as it
is CALLED, but only carries a result once it actually returned. So bash scripts/check-tools.sh <trace_id> SEND_MESSAGE prints VERDICT: PASS only when that tool ran AND returned a result;
UNCONFIRMED when the tool was dispatched but its span has no result (the classic
stalled-approval signature); and INCOMPLETE when it never ran. A tool NAME appearing in the
executed list is NOT proof it completed — a gated write can sit in the list with no result. And
for an external WRITE, even a returned result is only truly confirmed by reading the side effect
back (e.g. fetch the channel history). That read-back is the one certain check.
1---2name: build-agent3description: Turn a plain-language request into a working, tested Agenta agent through the Agenta API. Use when the user wants to build, create, schedule, or test an Agenta agent from their coding agent (Claude Code, Codex, Cursor). Writes one config and runs a few bundled shell scripts. Triggers on "build an Agenta agent", "create an agent on Agenta", "schedule an Agenta agent", or wiring an agent to a tool like GitHub or Slack via Agenta.4---56# Build an Agenta agent78You turn a plain-language request into a working, tested agent on Agenta. You do it by9writing one JSON config and running a few bundled scripts. **Optimize for the fewest calls10and the least time.** The simplest agent is two steps: write the config, run `build.sh`.11Do not over-think simple asks.1213Agenta is an open-source platform for building, testing, and deploying LLM apps and agents.14This skill builds Agenta *agents* over the Agenta HTTP API.1516**Use this skill first.** Everything you need is in this folder: the playbook below and the17scripts in `scripts/`. These facts were confirmed against a live Agenta deployment. Do not18browse the web or the codebase by default. Only if you get truly stuck do the open-source19repo (`https://github.com/Agenta-AI/agenta`) and docs (`https://docs.agenta.ai`) exist as a20fallback; prefer what is written here.2122## First run: credentials and prerequisites2324Do these two things once, before any build.2526**1. Credentials.** The scripts need `AGENTA_API_KEY`, and `AGENTA_HOST` only if the user27self-hosts (it defaults to Agenta cloud, `https://cloud.agenta.ai`).2829- Ask the user for their API key. Point them to it: the **API keys** page in their Agenta30 project settings (on cloud, under `https://cloud.agenta.ai`). Ask for the host URL only if31 they self-host, in which case it is their own Agenta domain.32- Then offer to set it up for them, either way they prefer:33 - **Write it for them:** create a `.env` file in the working directory with34 `AGENTA_API_KEY=...` (and `AGENTA_HOST=...` if self-hosting). `lib.sh` picks up a local35 `.env` automatically.36 - **Hand them a block to paste:** give them the lines to `export`, or the `.env` contents37 to create themselves.38- Never print or commit the key. Add `.env` to `.gitignore` if the directory is a repo.3940**2. Prerequisites.** Run `bash scripts/check-prereqs.sh`. It verifies `bash`, `curl`, and41`jq` are installed. On a miss it prints the exact install command for the user's platform;42show that to the user and ask them to install the tool, then continue.4344## The shape of every agent4546An agent is one JSON object, `parameters.agent`. You only ever decide four things inside it:47`instructions.agents_md` (who it is / what it does), `tools` (integration actions it can48call), `skills` (reusable know-how), and whether it needs a trigger (a schedule or an event).49The rest is fixed boilerplate — copy it exactly:5051```json52{53 "instructions": { "agents_md": "<what the agent is and does>" },54 "llm": { "model": "sonnet", "provider": "anthropic", "connection": { "mode": "self_managed" } },55 "tools": [],56 "mcps": [],57 "skills": [],58 "harness": { "kind": "claude" },59 "runner": { "kind": "sidecar", "interactions": { "headless": "auto" } },60 "sandbox": { "kind": "local" }61}62```6364Keep `llm` / `harness` / `runner` / `sandbox` exactly as above unless told otherwise. `model`65is an alias (`sonnet`, `opus`, `haiku`, `default`), never a raw model id. For the full config66field by field — tool entries, skill entries, and the fields that 500 if misplaced — read67`references/config-schema.md`.6869## The loop (run it in order, skip what the ask doesn't need)70711. **Read the ask. Decide what it needs** using the decision table below. Most asks need only72 instructions. Do not discover tools or triggers an ask does not call for.732. **If it needs integration actions, discover them** with `bash scripts/discover-tools.sh74 "<capability>"`. This prints the exact `tools` entries and each connection's state. Detail:75 `references/tools-and-connections.md`.763. **If a needed connection is not `ready`, STOP and ask the user to connect it.** Do not77 guess, do not fabricate a result. Report which integration needs connecting and why, then78 stop. That is a correct, complete outcome, not a failure.794. **Write the config** to a file under `configs/`. Integration actions go in `tools`,80 reusable know-how in `skills`, the behavior in `instructions.agents_md`. For a multi-tool81 or scheduled agent, write `instructions.agents_md` as an explicit numbered procedure that82 names the exact tools in order and ends on the terminal action — otherwise the run wanders83 or stops short. Depth in `references/writing-instructions.md`.845. **Create and test in one shot:** `bash scripts/build.sh <config> <slug> "<test message>"`.85 Read the OUTPUT and RESOLVED lines. RESOLVED must show86 `harness=claude model=sonnet connection=self_managed`.876. **If it needs a trigger**, create it against the variant id AND the revision id (both88 printed by `create-agent.sh` / `build.sh`) — a schedule with `bash scripts/create-schedule.sh`,89 an event subscription with `bash scripts/create-subscription.sh` (only after90 `discover-triggers.sh` and a `ready` connection). Passing the revision id is required: without91 it the trigger has no bound version to run, which the Agenta UI treats as an error. Pass the92 literal word `latest` in place of a revision id if you only have the variant id at hand.93 Confirm with `bash scripts/triggers.sh schedules` (or `subscriptions`).947. **Report** in a short bullet list: what you built, the artifact ids, what you tested, the95 result, and anything that needs the user.9697## Decision table9899| The ask… | Needs | What to add |100|---|---|---|101| transform text the user pastes (summarize, rewrite, classify) | nothing extra | `instructions` only |102| apply a body of know-how (a writing style, a review rubric) | a skill | one `skills` entry |103| read or write in an outside tool (GitHub, Slack, …) | gateway tools | `discover-tools.sh`, then `tools` entries |104| run on a clock (e.g. twice a day) | a schedule | build the agent, then `create-schedule.sh` |105| react to an outside event (new issue, new message) | a subscription | `discover-triggers.sh`, then `create-subscription.sh` (needs a ready connection) |106| change an agent that already exists | a new revision | `update-agent.sh` against the variant id, then re-test |107108## Scripts (your only interface to the API)109110Run them as `bash scripts/<name>` from this skill's directory. They wrap real Agenta111endpoints and load credentials themselves — you never handle the API key.112113- `check-prereqs.sh` — verify `bash`/`curl`/`jq` before you start.114- `build.sh <config.json> <slug> "<msg>" [name]` — create the agent AND test it. The fast path.115- `create-agent.sh <config.json> <slug> [name]` — create only; prints `application_id` /116 `variant_id` / `revision_id`. Use when you need the variant id before scheduling.117- `test-agent.sh <application_id> <config.json> "<msg>"` — batch invoke + verify. A single118 non-streaming call returns the full real turn, so it prints OUTPUT (the last assistant119 message), a `TOOLS:` line (every tool called, in order, with call/result counts), an120 `APPROVAL:` line when the run paused on an approval gate, RESOLVED (the config the run121 actually used), and the TRACE id.122- `discover-tools.sh "<capability>" ...` — find gateway tools for plain-language actions; shows123 each connection's state and the ready-to-wire tool object.124- `discover-triggers.sh "<event>" ...` — find event triggers (for subscriptions only).125- `update-agent.sh <variant_id> <config.json> [message]` — commit a new config to an EXISTING126 agent. Prefer this over archive-and-recreate: the slug, the ids, and any triggers survive.127- `create-schedule.sh <variant_id> <revision_id> "<cron UTC>" <event_key> [name] [inputs_json]`128 — cron trigger. The revision id (from `create-agent.sh` / `build.sh`) pins which committed129 version runs; omitting it leaves the schedule with no bound revision. Pass `latest` instead130 of a real id to have the script look up the variant's current HEAD revision for you.131- `create-subscription.sh <variant_id> <revision_id> <event_key> <connection> [name]132 [trigger_config_json] [inputs_json]` — event trigger. Same revision-id requirement (and133 `latest` shortcut) as `create-schedule.sh`. The connection (id or slug) must be `ready`134 first; verify actual fires with `triggers.sh deliveries`.135- `triggers.sh schedules|subscriptions|deliveries|rm-schedule <id>|rm-subscription <id>`.136- `check-tools.sh <trace_id> [terminal_tool]` — OPTIONAL fallback. `test-agent.sh` already lists137 the tools a run called (its `TOOLS:` line). Reach for this only to confirm a gated WRITE138 actually returned `ok:true` — pass the terminal tool for a PASS/INCOMPLETE verdict from the139 trace spans.140- `archive-agent.sh <application_id>` — soft-delete an app; use it to clean up or free a slug.141142Two extras live in `scripts/extras/` and stay out of the loop: `list-connections.sh`143(discover already prints the CONNECTIONS block; see `references/tools-and-connections.md`)144and `annotate-trace.sh` (an experimenter demo; see `references/annotate-trace.md`).145146## Deeper topics (read on demand)147148Keep to the loop above for a simple agent. Read one of these only when the task calls for it.149150| To do this | Read |151|---|---|152| Understand the full agent config field by field | `references/config-schema.md` |153| Discover tools, wire a connection, add an event trigger | `references/tools-and-connections.md` |154| Write instructions for a multi-tool or scheduled agent | `references/writing-instructions.md` |155| Shape the inputs a schedule or subscription passes to the run | `references/trigger-inputs.md` |156| Record a reflection against a trace | `references/annotate-trace.md` |157158## Hard rules (these prevent the usual failures)159160- **A trigger needs the revision id, not just the variant id.** `create-schedule.sh` and161 `create-subscription.sh` both take `<variant_id> <revision_id> ...` — pass both (printed by162 `create-agent.sh` / `build.sh`), or pass `latest` in place of the revision id if you only have163 the variant id at hand. Omitting the revision id creates a trigger with no bound version,164 which the Agenta UI treats as an error ("Which version runs?" required, unset).165- **Fewest calls.** A no-tools agent is exactly two actions: write the config, run `build.sh`.166 Don't re-list, don't re-verify facts already given here, don't re-read the schema.167- **Test before you declare done.** A claim with no `build.sh` / `test-agent.sh` behind it does168 not count. Check OUTPUT is right and RESOLVED shows the intended config.169- **Write the persona as an explicit imperative.** The agent is Claude Code underneath; on170 ambiguous input it falls back to a generic coding-assistant persona instead of doing the job.171 Author `instructions.agents_md` as an explicit imperative — who the agent is and what it does,172 stated as a command, not a vague topic. (Likewise phrase a one-off test with a verb:173 `"Summarize the following text:\n\n<text>"`, not a bare `<text>`, so the test isn't read as a174 chat opener.)175- **Crons are UTC, five fields, one-minute floor.** Convert the user's local time yourself.176- **needs_auth means stop.** If a needed connection is not `ready`, stop and ask the user to177 connect it. That is the whole job for that step.178- **Discovery is a search, not an oracle.** `discover-tools.sh` / `discover-triggers.sh` return179 high-recall best guesses over the live catalog. The matched **connection state** is reliable;180 the matched **integration and action** are not always right. A search matches on words: "save181 notes to Notion" can match Slack's `SEARCH_MESSAGES` as the primary tool and still print182 `READY: true` — while the Notion action you wanted is buried in `alternatives` and its183 connection is `needs_auth`. So: **confirm the matched tool's `integration` is the one you184 asked for**, and treat the per-integration `CONNECTIONS:` block (not the headline `READY`185 line) as the source of truth. If the match looks off, re-word the fragment or pick from the186 alternatives — a search for "new telegram message" can return a Slack event the same way.187 For `discover-triggers.sh` specifically, a right *integration* is not enough — the keyword188 match can land on the wrong *event within that integration* (e.g. "new github issue opened"189 matching a `GITHUB_ARTIFACT_CREATED_TRIGGER` event purely on the shared word "created", while190 the connection still reports `ready`). Always read the `fires_on` line the script prints for191 the MATCHED event, and the ALTERNATIVES block, before wiring anything into192 `create-subscription.sh` — a plausible-looking `event_key` with the right integration and a193 `ready` connection is not proof it fires on what you asked for. If nothing in MATCHED or194 ALTERNATIVES actually corresponds to the requested event, **stop and tell the user this195 integration doesn't support that trigger yet** — do not wire up the closest keyword hit.196- **Change an existing agent by committing, not recreating.** `update-agent.sh <variant_id>197 <config.json>` commits a new revision to the same app; the slug, the ids, and any schedules198 or subscriptions keep working. Archive-and-recreate loses all of them. Re-test after the199 update with `test-agent.sh`.200- **A failed build still creates the app.** If `build.sh` creates the app but the invoke fails,201 the app (and its slug) already exist. Fix the config and re-test with `test-agent.sh` against202 the existing `application_id` — do NOT re-run `build.sh` with the same slug (it conflicts).203 For a clean retry, `archive-agent.sh <application_id>` first.204- **Invoke carries the config inline (a lab artifact, not a gap).** `build.sh` and205 `test-agent.sh` inline the full config on every invoke against the low-level agent-service206 endpoint, which does not hydrate a committed reference. That part is unrelated to the207 endpoint/auth/body-shape contract itself, which is settled — see208 `docs/designs/invoke-negotiations/specs.md` in the `agenta` repo. The product API invoke path209 loads a committed reference server-side, so bare-reference invoke is a real product feature;210 this kit still doesn't use that path, and there's no need to rewrite the scripts to invoke by211 bare reference.212- **Report short.** A few bullets. Lead with the result and the artifact ids.213214## Verify (don't trust a 200)215216`test-agent.sh` sends a non-streaming invoke (`Accept: application/json`). The response's217`data.outputs` carries the run's full real turn, not just a final string: `messages` (assistant218text plus a `role: "tool"` entry for every tool call and result, in order), `stop_reason`, and —219only when the run paused on an approval gate — `pending_interaction`. The script reads this220directly, so OUTPUT / TOOLS / APPROVAL below come straight off the invoke response, no trace221needed. (Endpoint, auth, body shape, and this response contract are settled — see222`docs/designs/invoke-negotiations/specs.md` in the `agenta` repo. The script sends no extra223headers, so it gets the defaults: full transcript, resolved workflow embeds.)224225`test-agent.sh` also prints RESOLVED from the run's trace. It must match what you intended. If226it shows a different harness/model, the run silently fell back — fix the config and re-test.227Traces flush a second or two late; the script already retries.228229**Multi-tool agents — read the `TOOLS:` line, not just OUTPUT.** A run that makes several tool230calls can end on its tool-call turn, so OUTPUT (the last assistant message) is genuinely EMPTY —231that is not a failure or a parsing gap, it means the run's last turn was a tool call rather than232a reply. `test-agent.sh`'s `TOOLS:` line lists every tool the run CALLED, in order, with each233result's ok/ERROR (e.g. `github__LIST_COMMITS -> ok -> slack__SEND_MESSAGE -> ok (2 calls, 2234results)`). That line is your reliable "did it reach the terminal tool, and did that tool return235ok" signal. If the terminal tool is in the list with an ok result, the run got there and236finished; if it's missing, the run STOPPED SHORT (did the early reads, then wandered and never237reached the final action). If it stopped short, re-test with a blunter, numbered instruction —238the config is usually fine, the run just wandered. See `references/writing-instructions.md`.239240`test-agent.sh` prints an `APPROVAL:` line when `stop_reason == "paused"` — a WRITE (like241`SEND_MESSAGE`) tripped a human-approval gate. The run stopped there: that tool's call is in242TOOLS, but it has no matching result yet, because the run paused before executing it. The write243may not complete within this invoke at all (the approval can go unresolved).244245`check-tools.sh` is the **fallback** for what this invoke can't show yet: whether a gated tool246actually RETURNED after the fact. It reads the trace spans — a tool's span appears as soon as it247is CALLED, but only carries a result once it actually returned. So `bash scripts/check-tools.sh248<trace_id> SEND_MESSAGE` prints `VERDICT: PASS` only when that tool ran AND returned a result;249`UNCONFIRMED` when the tool was dispatched but its span has no result (the classic250stalled-approval signature); and `INCOMPLETE` when it never ran. **A tool NAME appearing in the251executed list is NOT proof it completed** — a gated write can sit in the list with no result. And252for an external WRITE, even a returned result is only truly confirmed by reading the side effect253back (e.g. fetch the channel history). That read-back is the one certain check.