One API
Bots
A bot is a saved workflow (kind: bot) that runs on a clock, answers messages, keeps its own memory and
files, and stays inside a policy you set. This chapter is the REST surface for all of it: save a bot, set
what it may do and spend, watch and answer its runs, schedule it, start it from outside, read what it
keeps, bind it to a chat, and call it by id. These routes read and write the same rows that Ocean Studio and
the MCP server show. Base URL is https://api.plungeai.com; every route takes an ozk_ key
(Authentication).
Four rules hold on every route in this chapter:
- Your rows only. The key's account is the tenant. Another account's id answers the same
404 <resource>_not_foundas an id that does not exist, with no hint (workflow_not_found,execution_not_found,schedule_not_found,trigger_not_found,group_not_found,template_not_found,skill_not_found,memory_not_found). - Four wires. JSON by default; YAML in (
Content-Type: text/yaml) and out on the same routes;format=orAcceptpicks the response format (json,yaml,markdown,text); an unknown format is400 invalid_format(Workflows). Two routes answer in their own type: the workspace file download (raw bytes) and the execution stream (server-sent events). - No storage text. A storage or upstream failure is a generic
502(storage_error,upstream_error,scheduler_error); the message never carries database or engine text. - One error shape.
{"error": {"code", "message"}}plus theX-Error-Codeheader (Errors, limits & headers).
| Section | Routes |
|---|---|
| 16.1 Lifecycle | /v1/workflows, /v1/workflows/{id}/versions |
| 16.2 Policy and budget | /v1/workflows/{id}/permissions |
| 16.3 Executions and approvals | /v1/executions |
| 16.4 Schedules and wake | /v1/schedules |
| 16.5 Triggers and hooks | /v1/triggers |
| 16.6 Workspace, inbox, bindings, groups | /v1/workflows/{id}/workspace-files, /inbox, /bindings, /v1/bot-groups, /v1/channels |
| 16.7 Memory and skills | /v1/memory, /v1/skills |
| 16.8 Templates | /v1/templates |
| 16.9 The one door | POST /v1/execute |
Several workflows that work as one company are a campus: buildings of offices that items walk through, with
gates where a person decides. They have their own surface, /v1/companies, in Companies. Their
documents (kind: campus, kind: building) are saved with the lifecycle routes of 16.1.
Source: orchestration/api-gateway/routes/ (saved.ts, bots.ts, executions.ts, schedules.ts,
triggers.ts, memory.ts, skills.ts, templates.ts, execute.ts); the scheduler behind schedules
and triggers is orchestration/scheduler/.
16.1 Lifecycle
kind is workflow, agent, bot or campaign. These routes read and write the rows Studio's sidebar
and the MCP plungeai_workflow tool show.
| Method and path | What it does |
|---|---|
POST /v1/workflows | save one: {name, kind, yaml, description?, folder?} → 201 {"workflow": {…}}, Location names it |
GET /v1/workflows | list yours: kind, search, folder, limit 1 to 200 (default 50), offset → {workflows, total, limit, offset, has_more} |
GET /v1/workflows/{id} | the row and its live YAML: {workflow, yaml} |
PATCH /v1/workflows/{id} | change any of name, description, yaml, kind, folder ("none" unfiles) |
DELETE /v1/workflows/{id} | 204. A bot also loses its memory and its scheduled jobs; a campaign its jobs, and its ledger is tombstoned |
GET /v1/workflows/{id}/versions | saved versions, newest first |
POST /v1/workflows/{id}/versions | save the current YAML as a version: {label?} → 201 |
POST /v1/workflows/{id}/versions/{versionId}/restore | make a version the live YAML again; the restore is recorded as a new version |
The YAML is validated against the CNL schema and the kind's lint before anything is written: a bot is
exactly one task on the one-agent runtime, and a bot naming a frozen runtime such as harness-agent
with posture is refused. A refusal is 422 invalid_workflow with errors and warnings, and the
stored row is unchanged. That holds for PATCH and restore as well, so no route writes a YAML the
lint would refuse. A kind: campaign create also takes campaign_config (the Studio campaign shape) and
sets up the ledger and its schedule.
Save as YAML. POST /v1/workflows and PATCH /v1/workflows/{id} also take the workflow itself as a
Content-Type: text/yaml body (application/yaml and application/x-yaml work too): the pure workflow YAML,
workflow: at the top, with no JSON string around it, so a bot's YAML is stored exactly as you wrote it and
reads back byte for byte from GET /v1/workflows/{id}. The metadata moves to query parameters: kind
(required on create; on PATCH it must equal the stored kind or be left out, otherwise 400 invalid_request),
name (required on create unless the YAML has workflow.name, which is used when ?name= is absent),
description and folder. Lint, versions and name checks are the JSON path's, so broken YAML is the same
422 invalid_workflow; campaign_config stays a JSON-only field, and the reply is the usual JSON envelope.
For example curl -X POST -H 'Content-Type: text/yaml' --data-binary @bot.yaml "https://api.plungeai.com/v1/workflows?kind=bot&name=Competitor%20Watch"
with your Authorization: Bearer header.
curl -s -X POST https://api.plungeai.com/v1/workflows \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"name":"Competitor Watch","kind":"bot","yaml":"workflow:\n name: Competitor Watch\n tasks:\n - type: harness\n id: watch\n agent: one-agent\n goal: \"Check {input} for pricing changes\"\n mission: |\n You are Competitor Watch. Reply NO-CHANGE when nothing changed.\n When your task is finished, call the task_complete tool with your final result.\n"}'
# → 201 {"workflow":{"id":"…","name":"Competitor Watch","kind":"bot","version":null,…}}Listing is read-only and live-verified:
curl -s "https://api.plungeai.com/v1/workflows?kind=bot&limit=3" \
-H "Authorization: Bearer $PLUNGE_API_KEY"curl -s https://api.plungeai.com/v1/workflows/00000000-0000-4000-8000-000000000001 \
-H "Authorization: Bearer $PLUNGE_API_KEY"16.2 Policy and budget
A bot's policy is stored on its row, and the engine loads it once per run, on every door: a run by
id (POST /v1/workflows/{id}/execute), the Agent Call door (16.9), a schedule, a
trigger, a chat message, MCP and Studio all stop at the same caps and holds. Both routes are owner-only.
| Method and path | What it does |
|---|---|
GET /v1/workflows/{id}/permissions | the stored policy: {classes, locks, budget_usd_run, budget_usd_month} plus effective (every class: the stored preset, else allow) |
PUT /v1/workflows/{id}/permissions | replace the fields you send; fields you leave out stay |
| Field | |
|---|---|
classes | a preset per permission class: send, write, pay, delete, each allow, ask, deny or hand_off. A class you leave out falls back to the bot's YAML, then to the default |
locks | classes an admin locked: a locked class lifts the bot's own allow to ask. Only a team admin over the workflow (project owner or admin, company owner or admin) can set it |
budget_usd_run | the cap on one run, in USD; null removes it |
budget_usd_month | the cap on a month, in USD; null removes it |
locks from anyone who is not an admin is 403 lock_admin_required and nothing in that body is
written, classes and the caps included. An invalid field is 422 invalid_params and nothing is
written. A run that reaches its cap stops at the next turn and its result says so
([run stopped at $… per-run budget cap after N turns …]).
curl -s -X PUT https://api.plungeai.com/v1/workflows/<bot id>/permissions \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"classes":{"send":"ask","pay":"deny"},"budget_usd_run":0.25,"budget_usd_month":10}'
# → {"classes":{"send":"ask","pay":"deny"},"locks":[],"budget_usd_run":0.25,"budget_usd_month":10}16.3 Executions and approvals
Every run is an execution, whichever door started it: the API, the CLI, MCP, Studio, a schedule. Its id is
the x-execution-id header of the call that started it (the workflow_id in the JSON answer; it is your
x-trace-id when you sent one), or the execution_id a schedule's run returns. A run can
stop to ask you something or to hold an action for approval; answering it is the human-in-the-loop (HITL)
step, and these routes are how an API client takes it.
| Method and path | What it does |
|---|---|
GET /v1/executions | yours, newest first: source, status (running, completed, failed, cancelled), workflow_id, limit, offset → {executions, count, limit, offset, has_more, next_offset} |
GET /v1/executions/{id} | the status: poll this |
GET /v1/executions/{id}/output | the final result; format=text is the bare content, markdown what the MCP plane renders |
GET /v1/executions/{id}/conversation | the turns added after the run by continue and followup |
GET /v1/executions/{id}/files | the files the run produced (spreadsheets, documents, decks) |
POST /v1/executions/{id}/continue | answer a paused run, or approve its held action |
POST /v1/executions/{id}/followup | message a run: queued while it runs, a new turn once it finished |
POST /v1/executions/{id}/steer | queue a message for a running execution |
POST /v1/executions/{id}/cancel | stop a running execution (cooperative: at its next turn or tool boundary) |
GET /v1/executions/{id}/stream | re-attach to a running execution as server-sent events |
DELETE /v1/executions/{id} | remove the history row and the stored result |
status is running, completed, incomplete, failed or cancelled. A run that finished its turn and
waits on you reads needs_input (a question) or needs_approval (a held action), with a continuation
that carries the question or the pending action (its summary, price and permission class).
Answering a paused run. POST …/continue takes {"answer": "…"} to reply to a question (or to deny
or redirect an approval), or {"approve": true} to approve the held action with its stored token.
allow_scope (with approve) says how far the approval reaches: once (this call), run (the same tool
for the rest of the run) or always (the bot's stored rule: set it with PUT …/permissions, 16.2);
any other value is once. The call answers when the agent has replied, up to about 30 seconds while an
approved action runs.
| Status and code | When |
|---|---|
400 missing_answer | neither answer nor approve: true |
409 not_paused | the run already completed: use followup |
410 session_expired | no conversation session is left (it expired, or the agent is not conversational) |
409 execution_finished | steer or stream on a run that is no longer running |
410 result_expired | output of a run whose stored result is gone |
steer and followup on a run in flight answer 202 {"execution_id": "…", "queued": true}: the message reaches the live loop
at its next turn. followup on a finished run starts a new turn in its conversation and answers 200
with the reply. stream sends status (first, then on change), progress (one event per interior step of
the tasks you name with ?task=<id>, up to four), done (the run ended, paused for you, or after five minutes with
reattach: true) and a : ping comment every 15 seconds. A synchronous run writes no interior steps, so
its stream carries status only; runs started with progress watching (async, MCP, Studio, schedules) write them.
POST …/cancel is the same operation as POST /v1/workflows/executions/{id}/cancel.
curl -s "https://api.plungeai.com/v1/executions?limit=3" \
-H "Authorization: Bearer $PLUNGE_API_KEY"curl -s https://api.plungeai.com/v1/executions/00000000-0000-4000-8000-000000000003 \
-H "Authorization: Bearer $PLUNGE_API_KEY"An approval round trip (a run held for approval, then approved for the rest of the run) changes state and spends, so it is not auto-verified:
curl -s https://api.plungeai.com/v1/executions/<id> -H "Authorization: Bearer $PLUNGE_API_KEY"
# → {"execution_id":"…","status":"needs_approval","continuation":{"status":"needs_approval","pending_action":{…}}}
curl -s -X POST https://api.plungeai.com/v1/executions/<id>/continue \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"approve":true,"allow_scope":"run"}'
# → {"execution_id":"…","status":"completed","content":"…"}16.4 Schedules and wake
| Method and path | What it does |
|---|---|
POST /v1/schedules | create a schedule of a saved workflow: cron, or one time with run_at |
GET /v1/schedules | list yours (status, job_type, workflow_id, limit ≤ 100, offset) |
GET /v1/schedules/{id} | one schedule and its ten latest runs |
PATCH /v1/schedules/{id} | change name, description, cron, timezone, status, parameters, ends_at |
POST /v1/schedules/{id}/pause · /resume | set status to paused / active |
POST /v1/schedules/{id}/run | run it now (also served at /execute) |
POST /v1/schedules/{id}/wake | fire its workflow once, optionally later |
GET /v1/schedules/{id}/runs | run history (status, limit, offset) |
DELETE /v1/schedules/{id} | stop and remove it; the run history stays readable |
Create
| Field | |
|---|---|
name, schedule | required. schedule is a cron expression, or @once with run_at |
workflow_id | the saved workflow or bot to run (required for the default job_type: "workflow") |
job_type | workflow (default) or heartbeat (then target is its spec, an object). Agent and query schedules are not created here: wrap the agent in a saved workflow first |
timezone | IANA zone the cron is read in (default UTC) |
run_at | ISO-8601 time with a UTC offset, in the future. Required when schedule is @once |
ends_at | ISO-8601 time or ms epoch after which the schedule stops running |
parameters | deliver targets, a per-run input, … |
description | free text |
parameters.deliver is a list of {channel, chat_id?, to?}; a bare channel string such as "slack"
is widened. channel is one of inapp, email, slack, telegram, whatsapp, discord, teams;
anything else is 422 invalid_params and nothing is created.
A cron schedule, every weekday at 09:00 Pacific, with a Slack delivery:
curl -s -X POST https://api.plungeai.com/v1/schedules \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"name":"Morning digest","workflow_id":"<workflow id>","schedule":"0 9 * * 1-5",
"timezone":"America/Los_Angeles","parameters":{"deliver":["slack"]}}'A one-time run, which removes itself after it fires:
curl -s -X POST https://api.plungeai.com/v1/schedules \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"name":"One-off report","workflow_id":"<workflow id>","schedule":"@once",
"run_at":"2026-10-06T09:00:00Z"}'Both answer 201 {"schedule": {…}}. next_run_at, last_run_at and ends_at are ms epoch.
The refusals:
| Status and code | When |
|---|---|
400 run_at_required | schedule is @once and there is no run_at |
400 run_at_past | run_at is not in the future |
400 invalid_request | the scheduler refused the rest (a bad cron, a run_at without an offset, …): the message says which |
404 workflow_not_found | no saved workflow with that id for this key |
422 invalid_params | a missing or malformed field; error.missing names the missing ones |
List and read
curl -s "https://api.plungeai.com/v1/schedules?limit=5" \
-H "Authorization: Bearer $PLUNGE_API_KEY"GET /v1/schedules/{id} answers {"schedule": {…}, "recent_runs": […]}. A run has id (the
scheduler's run id), status, started_at, ended_at, duration_ms, execution_id and error.
curl -s https://api.plungeai.com/v1/schedules/job_does_not_exist \
-H "Authorization: Bearer $PLUNGE_API_KEY"Change, pause, delete
PATCH takes only the fields it names; ends_at: null clears the end date. A schedule cannot become
@once (create a one-time schedule instead), and status is active or paused only: DELETE
removes it. DELETE answers 200 {"id": "…", "deleted": true}; after it GET is 404, while
GET /v1/schedules/{id}/runs still lists the history.
Run now
POST /v1/schedules/{id}/run runs the schedule's workflow once, immediately, and answers when the run
is accepted: {"run_id", "execution_id", "cln_workflow_id"}. An optional body {"input": "…"} replaces
the schedule's own input for this run. run_id is the scheduler's record (the id in /runs);
execution_id (cln_workflow_id is the same id) is the one the execution routes take, so a running
run can be stopped with POST /v1/executions/{execution_id}/cancel (16.3). A run that started and
failed is 502 execution_failed with the run_id in the envelope.
Wake
POST /v1/schedules/{id}/wake fires the schedule's saved workflow once through a separate one-time
job, without touching the schedule. A pending wake is re-timed, never stacked, so repeated wakes leave
one job.
curl -s -X POST https://api.plungeai.com/v1/schedules/<id>/wake \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"note":"the inbox has new mail","in":"10m"}'
# → {"job_id":"job_…","next_run_at":1790000000000}note becomes the run's input. in delays the wake (seconds, or "90s", "10m", "2h"); left out,
it fires on the scheduler's next tick. job_id is the one-time job and next_run_at its fire time.
A schedule with no saved workflow (a heartbeat) is 422.
16.5 Triggers and hooks
A schedule can also be started, or watched, from outside. Three kinds, one shape each under
/v1/triggers/{kind}; every call names the schedule with schedule_id (in the body for POST, in
the query for GET and DELETE).
| Kind | Mint | List | Revoke |
|---|---|---|---|
webhook — an HTTPS URL that runs the schedule | POST /v1/triggers/webhook | GET /v1/triggers/webhook?schedule_id= | DELETE /v1/triggers/webhook/{token}?schedule_id= |
email — an address that runs it | POST /v1/triggers/email | GET /v1/triggers/email?schedule_id= | DELETE /v1/triggers/email/{token}?schedule_id= |
hooks — signed run events sent to your URL | POST /v1/triggers/hooks | GET /v1/triggers/hooks?schedule_id= | DELETE /v1/triggers/hooks/{id}?schedule_id= |
Mint answers 201 with Cache-Control: no-store. A secret is in the mint response once and is
never listed again; keep it. Revoking an id that is not on the schedule is 404 trigger_not_found.
Webhook and email triggers also rotate: POST /v1/triggers/{webhook|email}/{token}/rotate
{"schedule_id": "…"} mints a successor with the same settings (new secret) and then revokes the old
token; the response carries rotated: "<old token>".
Webhook
curl -s -X POST https://api.plungeai.com/v1/triggers/webhook \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"schedule_id":"<id>","scheme":"github","prompt_template":"New PR: {pull_request.title}",
"filters":{"all":[{"path":"action","op":"equals","value":"opened"}]}}'
# → {"token":"…","secret":"…","scheme":"github","url":"https://…/trigger/<token>"}| Field | |
|---|---|
scheme | how the sender signs: generic (default, X-Webhook-Signature = hex HMAC-SHA256 of the raw body), github, gitlab, standard-webhooks |
prompt_template | the run's input; {dotted.path} fills in a field of the JSON payload |
filters | {all?: Rule[], any?: Rule[]}, Rule = {path, op: equals|contains|exists, value?}, at most 20 rules; a path starting headers. reads a request header. A payload that does not match is acknowledged (204) and does not run |
The sender POSTs to url with the signature header its scheme names. A bad signature or a timestamp
more than five minutes off is 401; a repeated delivery id within an hour is 409.
POST /v1/triggers/email takes an optional allowed_from (comma-separated addresses or domains) and
answers {"token", "address_local_part": "bot+<token>"}: the address is that local part at the Email
Routing domain your operator set up. allowed_from is a sender filter, weaker than a webhook signature.
Only schedules of a saved workflow take a webhook or email trigger.
Outbound hooks
POST /v1/triggers/hooks {"schedule_id", "url", "events": ["run.finished", "run.failed"]} registers
an HTTPS URL (public, no credentials in it; at most 10 per schedule) and answers {"id", "secret"}.
events is any of run.started, run.finished, run.failed, run.needs_approval. Each delivery
carries X-Ocean-Delivery-Id, X-Ocean-Timestamp and X-Ocean-Signature: sha256=<hex>, the HMAC-SHA256
of "<delivery id>.<timestamp>.<raw body>" under the secret: recompute it over the raw body, compare in
constant time and reject an old timestamp. Delivery is best effort (one retry on a network error, a
timeout, 408, 429 or 5xx) and covers scheduler-path runs: cron, triggers, /run. A one-time schedule
deletes itself when its run ends, and its hooks with it.
The receiver contract in full is orchestration/BOT-README.md, "Outbound lifecycle webhooks".
16.6 Workspace, inbox, bindings, groups
What a bot keeps and who it talks to, besides its memory. Every /v1/workflows/{id}/… route here is
owner-only.
| Method and path | What it does |
|---|---|
GET /v1/workflows/{id}/workspace-files | the files the bot keeps between runs (workspace: bot), newest first: {files, truncated, workspace_id} |
GET /v1/workflows/{id}/workspace-files/download?path= | one file as raw bytes, with its stored content type and Content-Disposition: attachment |
GET /v1/workflows/{id}/inbox | bot-to-bot messages queued for it: {messages}, read-only; the bot drains them at the start of its next run |
GET /v1/workflows/{id}/bindings | the paired chats bound to this bot |
POST /v1/workflows/{id}/bindings | bind a paired chat: {channel, sender_id} → 201 |
DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id} | unbind it: 200 {"deleted": true} |
GET /v1/channels | your paired chats per channel |
POST /v1/channels/pair | {channel} → {code, expires_at} |
GET /v1/bot-groups · POST /v1/bot-groups | list; create {name, members} → 201 |
PUT /v1/bot-groups/{id} · DELETE /v1/bot-groups/{id} | replace (same body and checks as create); delete (200 {"deleted": true}, the member bots are untouched) |
GET /v1/bot-groups/{id}/thread | the messages sent to the group, oldest first, read-only |
Workspace files. path must name a file the listing shows, inside the workspace. .., an absolute
path outside it and the encoded forms of either are 400 invalid_path; a path that is not listed is
404 file_not_found; a file over 20 MB is 413 file_too_large.
Channels and bindings. The channels are telegram, whatsapp, discord and teams. Pair a chat
first: POST /v1/channels/pair {"channel": "telegram"} returns a code that is good for 10 minutes; send
it to the channel's bot from the chat, and the chat appears in GET /v1/channels under
channels.telegram.targets with its sender_id. Then bind it: POST /v1/workflows/{id}/bindings
{"channel": "telegram", "sender_id": "…"}. A chat talks to one bot, so binding it again moves it. A
chat you did not pair is 422 not_paired; an account at its binding limit is 409 binding_limit; unbinding
a chat that is bound to another bot is 404 binding_not_found. Where channel bindings are not switched on
yet, GET answers {"bindings": [], "pending": true} and the writes answer 503 not_enabled.
Groups. A group is a named set of 2 to 6 of your own saved bots. A bot that calls message_bot with
to=group:<name> reaches every member. name is 1 to 40 letters, digits, _, . or - and is unique
per account (409 name_taken; a bad name is 422 invalid_name). members must all be your bots, with no
repeats (422 invalid_members). Another account's group id is 404 group_not_found.
curl -s https://api.plungeai.com/v1/bot-groups \
-H "Authorization: Bearer $PLUNGE_API_KEY"curl -s https://api.plungeai.com/v1/channels \
-H "Authorization: Bearer $PLUNGE_API_KEY"16.7 Memory and skills
Memory is the account's long-term memory: facts that every bot and agent you run can recall. (A
bot's own scratch memory is separate; it is wiped when the bot is deleted.) These routes read and write the
same stores as the MCP plungeai_memory tool.
| Method and path | What it does |
|---|---|
GET /v1/memory/recall?q=&limit= | your entries, optionally only those containing q: {entries: [{id, scope, text}], count} |
POST /v1/memory/remember | {text, scope?} with scope memory (default) or user (your profile) → 201; text already stored is a 200 no-op |
POST /v1/memory/forget | send exactly one of {id} or {text}: {removed: n} |
GET /v1/memory/runs?q=&limit= | full-text search of your past runs: {run_id, ts, goal, snippet} |
GET /v1/memory/runs/{id} | one past run's stored result |
An entry's id is mem_ plus twelve hex characters, derived from its scope and text. forget {id} removes
that one entry (404 memory_not_found if your stores do not hold it); when the entry's text is also part of
another entry the id is ambiguous and the answer is 409 memory_conflict, so use forget {text}, which
purges every entry containing the text from both stores. A write the store refuses (it is full, or the
text matches a blocked pattern) is 422 memory_rejected, with current_entries so you can consolidate.
Skills are private markdown skills you saved. They are injected into a mission with
skills: [<name>] (Skills, plugins & capability injection).
| Method and path | What it does |
|---|---|
GET /v1/skills | yours: {skills: [{id, description}], count}; platform skills are not listed |
POST /v1/skills/learn | save one: {name, body, description?} → 201. name is kebab-case, 2 to 64 characters; body is markdown, up to 65,536 characters |
DELETE /v1/skills/{id} | 204 |
Saving a name you already own replaces the skill; a name held by a platform skill or by another account is
409 skill_name_taken. A skill that is not yours (or does not exist) is 404 skill_not_found. If the
skill registry cannot answer, GET and DELETE are 502 registry_unavailable: that is not an empty list
and nothing was deleted. Having an agent distil a skill from a URL or a text is the MCP plungeai_learn
tool; this route saves a skill you wrote.
curl -s "https://api.plungeai.com/v1/memory/recall?limit=1" \
-H "Authorization: Bearer $PLUNGE_API_KEY"curl -s https://api.plungeai.com/v1/skills \
-H "Authorization: Bearer $PLUNGE_API_KEY"16.8 Templates
Starter workflows, agents and bots. A template is a public card; using one copies its YAML into your account as a saved workflow of your own.
| Method and path | What it does |
|---|---|
GET /v1/templates?kind=&category=&limit= | active templates, featured first: {templates: [{id, name, kind, category, difficulty, usage_count, description}], count}; kind is workflow, agent or bot (else 422 invalid_params) |
GET /v1/templates/{id} | {template, yaml} |
POST /v1/templates/{id}/use | {name?} → 201 {"workflow": {…}}: a new saved workflow of the template's kind |
A bot template keeps its own name; the others are named <template name> (copy). Pass name to choose. The
copy goes through the same lint as 16.1: a template whose YAML fails it is
422 invalid_workflow and nothing is created. An id that is not an active template is
404 template_not_found.
curl -s "https://api.plungeai.com/v1/templates?kind=bot&limit=3" \
-H "Authorization: Bearer $PLUNGE_API_KEY"16.9 The one door
POST /v1/execute is level 3 of the API (Overview): the body is the program, and the call
always runs through the engine, so a saved bot keeps its stored policy (16.2).
Nothing above is replaced by it: POST /v1/workflows/{id}/execute and POST /v1/agents/{id}/execute answer
exactly as before. Use level 2 when you manage a resource or call it by id; use the door when the body is
the program. The body is JSON or YAML.
| Body | Runs |
|---|---|
agent: <id> and input | your saved agent, from its live YAML (a saved agent takes only the keys below) |
agent: <id>@3 and input | snapshot 3 of your saved agent: a pinned version |
agent: <building block id> and that agent's own fields | a registry agent as a one-task workflow |
workflow: (an object, or a YAML string), or root name and tasks: | an inline workflow |
These root keys steer the call and never reach an agent: format, input, inputs, user_context,
stream, dry_run and on_behalf_of.
dry_run: truevalidates and reports without running:200 {"dry_run": true, "kind", "agent", "tasks": [{"id", "agent", "type"}], "warnings"}. No run starts, no execution row is written, nothing is spent. A workflow that fails validation is400 invalid_workflow.stream: trueanswers as server-sent events, whateverformatsays.on_behalf_of: <customer id>runs a connection-tier agent on the account connected under that customer id (7.3); only this door takes it, the per-id execute routes refuse it with400 invalid_body.
Every answer to a call the door resolved says what ran (a run, a dry_run, a fence refusal such as
404 unknown_agent or 403 agent_not_active; 400 missing_workflow and 404 agent_not_found come before it and
carry none): x-ocean-agent (the saved agent or the building block; absent for an inline
workflow), x-ocean-tasks (how many top-level tasks), and x-ocean-version (the snapshot number for a
pinned or saved agent, live for a saved agent with no snapshot, inline for a building block or a workflow
body). The same agent id is written to the request log, so a trace shows which agent a call ran.
| Status and code | When |
|---|---|
404 agent_not_found | agent: names no saved agent of yours (or snapshot) and no building block; another account's agent answers the same |
400 ambiguous_body | agent: next to workflow: or tasks: |
400 invalid_body | an agent that is not an id (letters, digits, _ . : -, up to 128, then optionally @<number>); dry_run or stream that is not a boolean; a field a saved agent does not take (fields names them) |
400 missing_workflow | none of agent, workflow or tasks |
403 agent_not_active | a named agent card is not status:active |
424 connection_required / credential_required | a connection-tier agent and no connected account: the error carries connect (7.2) |
curl -s -X POST https://api.plungeai.com/v1/execute \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"agent":"plan-no-such-agent@1","input":"hi"}'A saved bot, pinned to a snapshot, planned but not run (dry_run):
curl -s -X POST https://api.plungeai.com/v1/execute \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF'
agent: <bot id>@1
input: Check the pricing page
dry_run: true
EOF
# → dry_run: true
# kind: bot
# agent: <bot id>
# tasks:
# - {id: watch, agent: one-agent, type: harness}
# warnings: []A YAML body gets a YAML answer (the Content-Type mirror); send JSON, or format: json, for JSON. Without dry_run the call runs the bot. Both answers carry
x-ocean-agent: <bot id>, x-ocean-tasks: 1 and x-ocean-version: 1.