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.

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

RouteDoes
POST /v1/workflowssave {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/workflowslist {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}/versionssave the current YAML as a version {label?} (≤ 200 chars) → 201 {version}
POST /v1/workflows/{id}/versions/{versionId}/restoremake a version live again → 200 {workflow}; recorded as a new version; 404 version_not_found
POST /v1/workflows/{id}/executerun 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)

RouteDoes
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}/permissionsbody (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-filesthe 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}/inboxqueued 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/channelsyour 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)

RouteDoes
GET /v1/executionsnewest 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}/conversationturns added by continue/followup {execution_id, messages}
GET /v1/executions/{id}/filesthe 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}/cancelcooperative stop {success, cancelled, markers}; 409 not_running (same operation as POST /v1/workflows/executions/{id}/cancel)
GET /v1/executions/{id}/streamSSE 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

RouteDoes
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}/runrun 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}/wakefire 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}/rotatean 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}/rotatean 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

RouteDoes
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

RouteDoes
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/forgetexactly 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/skillsyour 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:

BodyRuns
agent: <id> + inputyour saved agent or bot, from its live YAML (it takes only the wire keys below)
agent: <id>@3 + inputsnapshot 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 codeWhen
404 agent_not_foundno saved agent of yours (or snapshot) and no building block with that id
404 unknown_agentan inline workflow names an agent the registry does not know
400 ambiguous_body / invalid_body / missing_workflowagent: 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_ofa 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_activea named agent card is parked
409 duplicate_execution_id · 413 payload_too_large · 429 rate_limited · 502 engine_errora reused x-trace-id; a body over 1 MiB; the tier cap (Retry-After); the engine failed
424 connection_required / credential_requireda 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.

Planned: TI-34

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