plungeai-api-setup
Bots, executions, schedules, connections and the Agent Call door — over REST
One API 2.3.0 puts everything a saved bot needs on REST, under the same ozk key (Authorization: Bearer ozkYOURKEY).
One API 2.3.0 puts everything a saved bot needs on REST, under the same ozk_ key (Authorization: Bearer ozk_YOUR_KEY). The live contract is GET https://api.plungeai.com/v1/openapi.json (tags
Workflows, Executions, Schedules, Triggers, Bots, Memory, Skills, Templates,
Connections, Agent Call) — regenerate clients from it, never copy routes from this page. The classic planes
(discovery, tools, agents, MCP, traces, models) are in route-map.md.
Rules on every route here: your rows only (another account's id answers 404 <thing>_not_found,
same as a missing one) · JSON by default, YAML in (Content-Type: text/yaml) and out on the same
routes; the format query or body field, or Accept, picks json | yaml | markdown | text (an unknown
value is 400 invalid_format) · an error is {"error":{"code","message"}} rendered in that same format, and
X-Error-Code: <code> is on every error: branch on the header · a storage or upstream failure is a generic
502 (storage_error, upstream_error, scheduler_error), never its text · a body over 1 MiB is
413 payload_too_large · reuse of an x-trace-id on an execute call is 409 duplicate_execution_id: send a fresh UUID.
Saved workflows and bots
| Route | Does |
|---|---|
POST /v1/workflows | save {name, kind, yaml, description?, folder?, campaign_config?} → 201 {workflow, warnings} and a Location header; kind is workflow, agent, bot or campaign (campaign_config with kind: campaign sets up the ledger and its schedule); folder names one or creates it |
GET /v1/workflows | list {workflows, total, limit, offset, has_more}: ?kind=workflow|agent|bot|campaign, search (≤ 200 chars, plain text), folder (exact), limit 1–200 (default 50), offset; a bad kind, limit or offset is 422 invalid_params |
GET /v1/workflows/{id} | the row and its live YAML {workflow, yaml} |
PATCH /v1/workflows/{id} | any of name, description, yaml, kind, folder; null clears description and folder (folder: "none" unfiles), null on anything else is 422; moving kind away from bot or campaign runs that kind's teardown |
DELETE /v1/workflows/{id} | 204; a bot also loses its memory and its jobs, a campaign its jobs (its ledger is tombstoned) |
GET /v1/workflows/{id}/versions | {versions}, newest first |
POST /v1/workflows/{id}/versions | save the current YAML as a version {label?} (≤ 200 chars) → 201 {version} |
POST /v1/workflows/{id}/versions/{versionId}/restore | make a version live again → 200 {workflow}; recorded as a new version; 404 version_not_found |
POST /v1/workflows/{id}/execute | run a saved workflow or bot by id (level 2) {input?, inputs?, user_context?, format?} → {success, workflow_id, final_task_id, request_id, content}; the workflow_id is the execution id (plungeai-workflows → references/api.md) |
The YAML is validated and linted for its kind before anything is written — on create, PATCH and restore: a
bot is ONE type: harness task on agent: one-agent; a refusal is 422 invalid_workflow with errors and
warnings, and the row is unchanged. A missing or malformed field is 422 invalid_params.
Bot policy, workspace, inbox, chats, groups (owner only)
| Route | Does |
|---|---|
GET /v1/workflows/{id}/permissions | {classes, locks, budget_usd_run, budget_usd_month, effective}; effective is the preset each class really runs under (allow, ask, deny, hand_off) |
PUT /v1/workflows/{id}/permissions | body (JSON or YAML) {classes: {send|write|pay|delete: allow|ask|deny|hand_off}, locks: [send|write|pay|delete], budget_usd_run, budget_usd_month} → the stored policy; fields you send replace, null clears a cap — never send 0 (stored, but the engine applies a cap only above 0). locks need a team admin (else 403 lock_admin_required, nothing written); a bad field is 422 invalid_params |
GET /v1/workflows/{id}/workspace-files | the files the bot keeps, newest first {files, truncated, workspace_id} |
GET /v1/workflows/{id}/workspace-files/download?path= | one listed file as raw bytes, Content-Disposition: attachment; 400 invalid_path, 404 file_not_found, 413 file_too_large (20 MB cap) |
GET /v1/workflows/{id}/inbox | queued bot-to-bot messages {messages} (read-only) |
GET /v1/workflows/{id}/bindings | {bindings, pending}; pending: true with an empty list means channel bindings are not enabled yet |
POST /v1/workflows/{id}/bindings | {channel, sender_id} (≤ 128 chars) → 201 {binding}; a chat talks to ONE bot, so binding it again moves it |
DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id} | unbind → 200 {deleted}; 404 binding_not_found for a chat bound elsewhere |
GET /v1/channels | your paired chats {channels}; targets[].sender_id is what a binding takes |
POST /v1/channels/pair | {channel: telegram|whatsapp|discord|teams} → {code, expires_at}; send the code from the chat within 10 minutes |
GET /v1/bot-groups, POST /v1/bot-groups | {groups}; create {name, members} → 201 {group}: 2–6 distinct bots of yours, name 1–40 letters, digits, _, ., -, for message_bot to=group:<name> |
PUT /v1/bot-groups/{id}, DELETE /v1/bot-groups/{id} | replace (same body and checks) → 200 {group}; delete → 200 {deleted}, the member bots stay |
GET /v1/bot-groups/{id}/thread | {messages}, oldest first, read-only |
The engine loads the stored policy once per run, so every door (REST, MCP, CLI, Studio, schedules, triggers, chat) stops at the same caps and holds. A run that reaches its cap says so in its result.
Executions (every run, whichever door started it)
| Route | Does |
|---|---|
GET /v1/executions | newest first {executions, count, limit, offset, has_more, next_offset}: source (api, cli, mcp, studio, …), status (running, completed, failed, cancelled; else 400 invalid_status), workflow_id, limit ≤ 100 (default 20), offset |
GET /v1/executions/{id} | poll this: {execution_id, status, error_message, workflow_name, started_at, duration_ms, final_task_id, continuation}; status is running, completed, incomplete, failed, cancelled, or needs_input / needs_approval when the run waits on you (continuation carries the question or the held action) |
DELETE /v1/executions/{id} | 200 {success, deleted} (history row, result, follow-up thread); a second call is 404 |
GET /v1/executions/{id}/output | {execution_id, status, content, final_task_id, steps, continuation}; 404 not_ready while running, 409 execution_failed, 410 result_expired |
GET /v1/executions/{id}/conversation | turns added by continue/followup {execution_id, messages} |
GET /v1/executions/{id}/files | the Office/PDF files the run produced {execution_id, files} |
POST /v1/executions/{id}/continue | {answer?, approve?, allow_scope?} (answer ≤ 65536 chars): answer a paused run, or approve its held action; allow_scope once (default), run, always; 400 missing_answer, 409 not_paused (finished run: use followup), 410 session_expired, 502 continue_failed; runs inline, up to ~30 s |
POST /v1/executions/{id}/followup | {text} (≤ 65536): 202 {execution_id, queued} while running, 200 with the reply once finished; 400 missing_text |
POST /v1/executions/{id}/steer | {text}: queue a message for the live loop → 202 {execution_id, queued}; 409 execution_finished on a finished run |
POST /v1/executions/{id}/cancel | cooperative stop {success, cancelled, markers}; 409 not_running (same operation as POST /v1/workflows/executions/{id}/cancel) |
GET /v1/executions/{id}/stream | SSE re-attach: events status, progress (steps of the tasks named by ?task=, up to 4 ids), done, error, a : ping every 15 s; 409 execution_finished, 429 too_many_streams (5 per run) |
By-id reads of a run made on a customer's behalf (POST /v1/execute with on_behalf_of) take
?on_behalf_of=<customer id>: GET /v1/executions/{id}, GET /v1/executions/{id}/output and GET /v1/executions/{id}/files only (a bad id is
400 invalid_on_behalf_of).
Schedules, triggers, hooks
| Route | Does |
|---|---|
POST /v1/schedules | {name, schedule, workflow_id?, job_type?, target?, description?, timezone?, parameters?, run_at?, ends_at?} → 201 {schedule}; job_type is workflow (default, needs workflow_id) or heartbeat (target is its spec object); schedule: "@once" needs run_at: ISO-8601 WITH a UTC offset, in the future (400 run_at_required, 400 run_at_past; no offset is 400 invalid_request); parameters.deliver is [{channel, chat_id?, to?}], channel one of inapp, email, slack, telegram, whatsapp, discord, teams; 404 workflow_not_found |
GET /v1/schedules | {schedules, total, limit, offset, has_more}: status (active, paused), job_type (workflow, heartbeat, agent, query), workflow_id, limit 1–100 (default 20), offset |
GET /v1/schedules/{id} | {schedule, recent_runs} (the ten latest runs) |
PATCH /v1/schedules/{id} | any of name, description, schedule (a cron: a schedule cannot become @once), timezone, status (active, paused), parameters (replaces), ends_at (null clears) |
DELETE /v1/schedules/{id} | 200 {id, deleted}; the run history stays readable at /runs |
POST /v1/schedules/{id}/pause, POST /v1/schedules/{id}/resume | {schedule} with status paused / active |
POST /v1/schedules/{id}/run | run now {input?} → {run_id, execution_id, cln_workflow_id}: run_id is the id in /runs, execution_id is what the execution routes take; a run that started and failed is 502 execution_failed with run_id |
POST /v1/schedules/{id}/wake | fire the workflow once {note?, in?}: note becomes the input, in delays it (seconds or "10m"); a pending wake is re-timed, never stacked → {job_id, next_run_at}; 422 for a heartbeat |
GET /v1/schedules/{id}/runs | {runs, total, limit, offset, has_more}: status (running, completed, failed, cancelled), limit 1–100 (default 20), offset; execution_id is what the execution routes take |
GET /v1/triggers/webhook, POST /v1/triggers/webhook, DELETE /v1/triggers/webhook/{token}, POST /v1/triggers/webhook/{token}/rotate | an HTTPS URL that runs the schedule. Mint {schedule_id, prompt_template?, scheme?, filters?} → 201 {token, secret, scheme, url}; scheme is generic (default), github, gitlab or standard-webhooks; rotate returns the successor with rotated |
GET /v1/triggers/email, POST /v1/triggers/email, DELETE /v1/triggers/email/{token}, POST /v1/triggers/email/{token}/rotate | an address that runs it. Mint {schedule_id, allowed_from?} → 201 {token, address_local_part} (the local part is bot+<token>) |
GET /v1/triggers/hooks, POST /v1/triggers/hooks, DELETE /v1/triggers/hooks/{id} | signed run events to your https URL. Mint {schedule_id, url, events?} → 201 {id, secret}; events run.started, run.finished, run.failed, run.needs_approval; verify X-Ocean-Signature; 503 unavailable where hooks are not on yet |
Every trigger call names the schedule with schedule_id (body for mint and rotate, query for list and
delete); a secret is shown once, on mint, and the mint answer is never cached; 404 trigger_not_found for a token not on that schedule,
404 schedule_not_found for a schedule that is not yours. A scheduler failure is 502 scheduler_error; a refused setting is 400 invalid_request.
Connections — your keys, or your customer's
| Route | Does |
|---|---|
GET /v1/connections | {connections}: status only, never a credential; ?on_behalf_of=<customer id> lists one customer's |
POST /v1/connections/{provider} | bring a key {api_key, api_secret?, on_behalf_of?} (≤ 4096 chars each) → 201 {provider, on_behalf_of, status: "connected"}; checked live and stored encrypted; 400 credential_rejected / missing_api_key, 404 unknown_provider |
DELETE /v1/connections/{provider} | disconnect → 204; ?on_behalf_of= for a customer's; 404 connection_not_found |
POST /v1/connections/{provider}/link | {on_behalf_of?, return_url?} (https) → 201 {url, expires_at}: a one-use, 15-minute hosted connect URL to send a customer; 400 invalid_return_url |
provider is a connector id (google, azure for Microsoft 365, github, slack, stripe, …). A connection-tier
agent with no connection answers 424 connection_required (or credential_required) with
error.connect = {provider, method: oauth|api_key|wallet, link} naming where to connect it. on_behalf_of (1–128 chars of
A-Za-z0-9_.-, no ..; 400 invalid_on_behalf_of otherwise) is accepted on POST /v1/execute among the execute routes: the
run reads the credential connected under that customer id. /v1/workflows/{id}/execute, /v1/agents/{id}/execute
and /v1/tools/{id}/execute refuse it with 400 invalid_body. 503 connections_unavailable where connections are off.
Other errors you will meet
503 not_enabled (bindings, bot groups, channel pairing before their migration or deploy) · 409 binding_limit,
409 unbind_failed, 422 not_paired (bind a chat you did not pair) · 409 name_taken, 422 invalid_name /
invalid_members (bot groups) · 502 upstream_error (a store failed). The live spec lists each per operation.
Memory, skills, templates
| Route | Does |
|---|---|
GET /v1/memory/recall | ?q=&limit= (1–100, default 50) → {entries, count} |
POST /v1/memory/remember | {text (1–65536 chars), scope?: memory|user} → 201 {id, scope, text} (200 when the text is already stored); 422 memory_rejected (store full or blocked text) |
POST /v1/memory/forget | exactly one of {id} or {text} → {removed}; 404 memory_not_found, 409 memory_conflict |
GET /v1/memory/runs, GET /v1/memory/runs/{id} | ?q= (required) &limit= (1–100, default 10) → {runs, count}; one past run {run_id, content}, 404 run_not_found |
GET /v1/skills | your private skills {skills, count}; 502 registry_unavailable is not an empty list |
POST /v1/skills/learn | {name (^[a-z0-9][a-z0-9-]{1,63}$), body (≤ 65536), description? (≤ 60)} → 201 {skill}; 409 skill_name_taken |
DELETE /v1/skills/{id} | 204; 404 skill_not_found |
GET /v1/templates | ?kind=workflow|agent|bot&category=&limit= (1–100, default 50) → {templates, count} |
GET /v1/templates/{id} | {template, yaml}; 404 template_not_found |
POST /v1/templates/{id}/use | {name?} (1–512) → 201 {workflow}: a new saved workflow of the template's kind; a bot template keeps its name, the others are <template> (copy); 422 invalid_workflow if its YAML fails the lint |
Detail: plungeai-memory, plungeai-skills-plugins, plungeai-discovery.
The Agent Call door — POST /v1/execute (level 3)
The body (JSON, or YAML for any other Content-Type) is the program, and the call always runs through the engine, so a saved bot keeps its stored policy:
| Body | Runs |
|---|---|
agent: <id> + input | your saved agent or bot, from its live YAML (it takes only the wire keys below) |
agent: <id>@3 + input | snapshot 3 of it (a pinned version) |
agent: <building block id> + that agent's own fields (or a bare agent: one-agent + prompt) | a registry agent as a one-task workflow |
workflow: (object or YAML string), or root name + tasks: (in YAML, the workflow itself at the root) | an inline workflow |
Root keys that steer the call and never reach an agent: format, input, inputs, user_context,
stream (true answers as SSE), dry_run (200 {dry_run, kind, agent, tasks, warnings}: no run, no row,
no spend), on_behalf_of. Answers carry x-ocean-agent, x-ocean-tasks, x-ocean-version (<n>, live or
inline). A run answers {success, workflow_id, final_task_id, request_id, content}.
curl -s -X POST https://api.plungeai.com/v1/execute \
-H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"agent":"<saved bot id>@3","input":"Check the pricing page","dry_run":true}'| Status and code | When |
|---|---|
404 agent_not_found | no saved agent of yours (or snapshot) and no building block with that id |
404 unknown_agent | an inline workflow names an agent the registry does not know |
400 ambiguous_body / invalid_body / missing_workflow | agent: next to workflow: or tasks:; an unreadable body, extra fields next to a saved agent, a malformed agent ref, a non-boolean dry_run/stream; none of agent, workflow, tasks |
400 reserved_field / invalid_workflow / invalid_json / invalid_yaml / invalid_format / invalid_on_behalf_of | a field named like an engine task key; dry_run found the workflow invalid; a body that does not parse; an unknown format; a bad customer id |
403 agent_not_active | a named agent card is parked |
409 duplicate_execution_id · 413 payload_too_large · 429 rate_limited · 502 engine_error | a reused x-trace-id; a body over 1 MiB; the tier cap (Retry-After); the engine failed |
424 connection_required / credential_required | a connection-tier agent and no connected account (connect in the error) |
The per-resource execute routes (/v1/workflows/{id}/execute, /v1/agents/{id}/execute) answer
exactly as before: the three levels all stay. Start with level 2; use the door when the body is
the program.