For AI agents: a documentation index is available at https://www.plungeai.com/docs/llms.txt. Append .md to any page URL, or send Accept: text/markdown, to get markdown. Setup instructions for agents are at https://www.plungeai.com/docs/agents.md. One ozk_ key opens every plane, models included.

Documentation Index: fetch the complete documentation index at /llms.txt. Use this file to discover all available pages before exploring further.

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_found as 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= or Accept picks the response format (json, yaml, markdown, text); an unknown format is 400 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 the X-Error-Code header (Errors, limits & headers).
SectionRoutes
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 doorPOST /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 pathWhat it does
POST /v1/workflowssave one: {name, kind, yaml, description?, folder?} → 201 {"workflow": {…}}, Location names it
GET /v1/workflowslist 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}/versionssaved versions, newest first
POST /v1/workflows/{id}/versionssave the current YAML as a version: {label?} → 201
POST /v1/workflows/{id}/versions/{versionId}/restoremake 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 pathWhat it does
GET /v1/workflows/{id}/permissionsthe stored policy: {classes, locks, budget_usd_run, budget_usd_month} plus effective (every class: the stored preset, else allow)
PUT /v1/workflows/{id}/permissionsreplace the fields you send; fields you leave out stay
Field
classesa 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
locksclasses 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_runthe cap on one run, in USD; null removes it
budget_usd_monththe 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 pathWhat it does
GET /v1/executionsyours, 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}/outputthe final result; format=text is the bare content, markdown what the MCP plane renders
GET /v1/executions/{id}/conversationthe turns added after the run by continue and followup
GET /v1/executions/{id}/filesthe files the run produced (spreadsheets, documents, decks)
POST /v1/executions/{id}/continueanswer a paused run, or approve its held action
POST /v1/executions/{id}/followupmessage a run: queued while it runs, a new turn once it finished
POST /v1/executions/{id}/steerqueue a message for a running execution
POST /v1/executions/{id}/cancelstop a running execution (cooperative: at its next turn or tool boundary)
GET /v1/executions/{id}/streamre-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 codeWhen
400 missing_answerneither answer nor approve: true
409 not_pausedthe run already completed: use followup
410 session_expiredno conversation session is left (it expired, or the agent is not conversational)
409 execution_finishedsteer or stream on a run that is no longer running
410 result_expiredoutput 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 pathWhat it does
POST /v1/schedulescreate a schedule of a saved workflow: cron, or one time with run_at
GET /v1/scheduleslist 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 · /resumeset status to paused / active
POST /v1/schedules/{id}/runrun it now (also served at /execute)
POST /v1/schedules/{id}/wakefire its workflow once, optionally later
GET /v1/schedules/{id}/runsrun history (status, limit, offset)
DELETE /v1/schedules/{id}stop and remove it; the run history stays readable

Create

Field
name, schedulerequired. schedule is a cron expression, or @once with run_at
workflow_idthe saved workflow or bot to run (required for the default job_type: "workflow")
job_typeworkflow (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
timezoneIANA zone the cron is read in (default UTC)
run_atISO-8601 time with a UTC offset, in the future. Required when schedule is @once
ends_atISO-8601 time or ms epoch after which the schedule stops running
parametersdeliver targets, a per-run input, …
descriptionfree 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 codeWhen
400 run_at_requiredschedule is @once and there is no run_at
400 run_at_pastrun_at is not in the future
400 invalid_requestthe scheduler refused the rest (a bad cron, a run_at without an offset, …): the message says which
404 workflow_not_foundno saved workflow with that id for this key
422 invalid_paramsa 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).

KindMintListRevoke
webhook — an HTTPS URL that runs the schedulePOST /v1/triggers/webhookGET /v1/triggers/webhook?schedule_id=DELETE /v1/triggers/webhook/{token}?schedule_id=
email — an address that runs itPOST /v1/triggers/emailGET /v1/triggers/email?schedule_id=DELETE /v1/triggers/email/{token}?schedule_id=
hooks — signed run events sent to your URLPOST /v1/triggers/hooksGET /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
schemehow the sender signs: generic (default, X-Webhook-Signature = hex HMAC-SHA256 of the raw body), github, gitlab, standard-webhooks
prompt_templatethe 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.

Email

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 pathWhat it does
GET /v1/workflows/{id}/workspace-filesthe 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}/inboxbot-to-bot messages queued for it: {messages}, read-only; the bot drains them at the start of its next run
GET /v1/workflows/{id}/bindingsthe paired chats bound to this bot
POST /v1/workflows/{id}/bindingsbind a paired chat: {channel, sender_id} → 201
DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}unbind it: 200 {"deleted": true}
GET /v1/channelsyour paired chats per channel
POST /v1/channels/pair{channel} → {code, expires_at}
GET /v1/bot-groups · POST /v1/bot-groupslist; 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}/threadthe 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 pathWhat 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/forgetsend 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 pathWhat it does
GET /v1/skillsyours: {skills: [{id, description}], count}; platform skills are not listed
POST /v1/skills/learnsave 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 pathWhat 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.

BodyRuns
agent: <id> and inputyour saved agent, from its live YAML (a saved agent takes only the keys below)
agent: <id>@3 and inputsnapshot 3 of your saved agent: a pinned version
agent: <building block id> and that agent's own fieldsa 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: true validates 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 is 400 invalid_workflow.
  • stream: true answers as server-sent events, whatever format says.
  • 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 with 400 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 codeWhen
404 agent_not_foundagent: names no saved agent of yours (or snapshot) and no building block; another account's agent answers the same
400 ambiguous_bodyagent: next to workflow: or tasks:
400 invalid_bodyan 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_workflownone of agent, workflow or tasks
403 agent_not_activea named agent card is not status:active
424 connection_required / credential_requireda 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.

Planned: TI-34

The docs assistant is not available yet. You can hand these docs to your own assistant instead.