# PlungeAI API Overview Source: https://www.plungeai.com/docs/getting-started/overview ## Set up PlungeAI via MCP One server, 22 tools. Needs an ozk_ key from Dashboard → One API → Keys. Works in every major coding agent. [See all install options →](/developer-tools/mcp/quickstart) **Run in terminal.** Run this command in your terminal. Set PLUNGE_API_KEY first; the header is stored in your Claude Code config. ```bash claude mcp add --transport http plungeai https://mcp.plungeai.com/v1 --header "Authorization: Bearer $PLUNGE_API_KEY" ``` **Verify installation:** Run /mcp to see plungeai, then ask: use plungeai_whoami to confirm my identity [Learn more](/integrations/claude-code) **Run in terminal.** Run this command in your terminal. Codex reads the key from PLUNGEAI_API_KEY at run time: export PLUNGEAI_API_KEY=$PLUNGE_API_KEY. ```bash codex mcp add plungeai --url https://mcp.plungeai.com/v1 --bearer-token-env-var PLUNGEAI_API_KEY ``` **Verify installation:** /mcp lists plungeai [Learn more](/integrations/codex) **One-click install.** Add PlungeAI to Cursor: [Add to Cursor →](https://mcp.plungeai.com/install/cursor) Then add the Authorization header in ~/.cursor/mcp.json (user-level file, never a repo file). **Verify installation:** In agent chat: use plungeai_whoami to confirm my identity [Learn more](/integrations/cursor) **One-click install.** Add PlungeAI to VS Code: [Add to VS Code →](https://mcp.plungeai.com/install/vscode) Or from a terminal: ```bash code --add-mcp "{\"name\":\"plungeai\",\"type\":\"http\",\"url\":\"https://mcp.plungeai.com/v1\",\"headers\":{\"Authorization\":\"Bearer $PLUNGE_API_KEY\"}}" ``` **Verify installation:** In Copilot agent chat: plungeai_whoami [Learn more](/integrations/vscode) These docs use PLUNGE_API_KEY; the PlungeAI skills, the Claude Code plugin and the install page use PLUNGEAI_API_KEY; any name works if your config and shell agree. ## Onboard your agent Connect PlungeAI by pasting this prompt to your agent. ```text Use curl to read https://www.plungeai.com/docs/agents.md and perform the setup to connect PlungeAI ``` The file it reads, [/agents.md](/agents.md), asks you for a key, connects your client to the MCP server, verifies the connection with `plungeai_whoami` and lists the rules the agent follows. ## Build with PlungeAI APIs Pick a product and open its quickstart. Not sure which API to use? Try the [API chooser](/getting-started/choose-an-api). [Create an API key](https://dashboard.plungeai.com/one-api?tab=keys) OpenAI-compatible chat with fallback and presets. Key: `ozk_`. A typed tool call behind a trust fence. Key: `ozk_`. One prompt, sync, async or streamed. Key: `ozk_`. Multi-agent CNL pipelines, inline or saved. Key: `ozk_`. An agent loop inside allowed tools and iterations. Key: `ozk_`. Cron agents, heartbeats and notifications. Key: `ozk_`. Find agents, tools and models before you call. Key: `ozk_`. # Choose an API Source: https://www.plungeai.com/docs/getting-started/choose-an-api ## What are you building? Find your job in the first column, answer its question, and start with the API the answer names. | Job | Question | Answer → start with | |---|---|---| | **Call a model from my code**: chat or embeddings in my app | Do you need one fixed model, or automatic fallback? | One model → [Chat completions](#start-with-chat-completions) · Fallback, cheapest or fastest → [Model routing](#start-with-model-routing) | | **Run one capability**: search, scrape, send, look up | Do you already know which tool? | Yes → [Tools](#start-with-tools) · No → [Discovery](#start-with-discovery) | | **Get an answer from an agent**: one prompt in, an answer out | Do you need the answer now, or can it arrive later? | Now (or streamed) → [Agents (sync)](#start-with-agents-sync) · Later → [Agents (async)](#start-with-agents-async) | | **Chain several agents**: a pipeline with fixed steps, or an open goal | Should an agent choose the steps itself, within limits? | No, fixed steps → [Workflows](#start-with-workflows) · Yes → [Missions](#start-with-missions) | | **Run something on a schedule**: every hour, every day, on a condition | none | [Scheduling](#start-with-scheduling) | | **Work from my coding agent**: Claude Code, Cursor, Codex, VS Code | none | [the MCP server](#start-with-the-mcp-server) | The interactive chooser is planned (TI-74); until then this table and the results below carry the same data. ### Start with Chat completions You call one model you already chose through the OpenAI-compatible chat API, so any OpenAI SDK works once you change the base URL. **How to start:** base URL `https://api.plungeai.com/v1`; key `$PLUNGE_API_KEY` (your `ozk_` key); `model` set to a slug from `GET /v1/models`; `stream: true` if you want SSE chunks. [Setup details →](/models/quickstart) · [Build with Chat completions →](/models/quickstart) | | | |---|---| | What you send | `POST /v1/chat/completions` with `model`, `messages` and your `ozk_` key | | What you get back | `chat.completion` (or SSE chunks) with `usage` | | What your app handles | Retries on 429 `rate_limit_exceeded` and 5xx; model choice | | How long it takes | One model call; streaming starts at the first token | | What you pay for | Tokens, billed at provider cost × your org markup ([Pricing](/getting-started/pricing)) | | What to check | `x-request-id`, `usage`, served `model` ([Models quickstart](/models/quickstart)) | **Before you build:** the models plane takes the same `ozk_` key as every other plane; a missing or invalid key answers `401 invalid_api_key`. ### Start with Model routing The gateway tries your candidates in order, or sorts them by price, latency or throughput, so one failing provider does not fail the request. **How to start:** the Chat completions setup plus `models: ["", ""]` and `sort` set to `price`, `latency` or `throughput`, or `model: "@preset/"` for a saved preset; the Python SDK passes these in `extra_body`. [Setup details →](/models/routing) · [Build with Model routing →](/models/routing) | | | |---|---| | What you send | The same request, plus `models[]`, `sort` or `model: "@preset/"` | | What you get back | The completion from the first candidate that succeeds; `model` names the one that served | | What your app handles | Reading which model served | | How long it takes | One call, plus failover attempts | | What you pay for | Tokens of the model that served | | What to check | The `model` field, and 502 "All routing candidates failed" | **Before you build:** read the served `model` field before you log costs; it can differ from your first candidate. ### Start with Tools You already know the capability, and one tool call gives typed params, one outcome object and the trust fence in a single request. **How to start:** key `$PLUNGE_API_KEY` (an `ozk_` key); read `GET /v1/tools/{id}` for `operations[]` and `inputSchema`; send `{operation, params}` to `POST /v1/tools/{id}/execute`. [Setup details →](/tools/quickstart) · [Build with Tools →](/tools/quickstart) | | | |---|---| | What you send | `POST /v1/tools/{id}/execute {operation, params}` with an `ozk_` key | | What you get back | `{ok, content, outcome, execution_id, request_id}` | | What your app handles | 422 `invalid_params`, 409 `approval_required`, 424 `connection_required` | | How long it takes | One tool run; `mode: async` over MCP for long ones | | What you pay for | Counts toward tier POST limits; per-call price TBD by owner (TI-31) | | What to check | `outcome`, `execution_id`, the trace ([Outcomes](/tools/outcomes)) | **Before you build:** search first; never hard-code a tool id. Handle 409 `approval_required` and 424 `connection_required` before you ship. ### Start with Discovery You do not know the right tool or agent yet, and search returns ranked cards you can read before you spend a call. **How to start:** `GET /v1/discovery/search` with `q` (your need in plain words), `kind` (for example `agents`), `mode` `hybrid` (or `keyword`, `vector`), `fields` `summary` and `limit`; page with `offset`. [Setup details →](/discovery/core-concepts/search-modes) · [Build with Discovery →](/discovery/quickstart) | | | |---|---| | What you send | `GET /v1/discovery/search?q=&kind=` | | What you get back | `{cards, count, searchMethod, query}` | | What your app handles | Picking an id, then reading the contract | | How long it takes | One synchronous GET | | What you pay for | GETs are not rate-limited; price TBD by owner | | What to check | `status:active` on the card ([Card status](/discovery/core-concepts/card-status)) | **Before you build:** call only cards with `status: active`; search filters on `active` by default, so widen `status` only on purpose. ### Start with Agents (sync) One prompt in and one answer out on the same connection is the shortest path to a registry agent, and `stream: true` shows tokens as they arrive. **How to start:** key `$PLUNGE_API_KEY`; `POST /v1/agents/one-agent/execute` with `prompt`, `sync: true` (the default) and `max_tokens`; add `stream: true` for OpenAI chunks. [Setup details →](/agents/quickstart) · [Build with Agents →](/agents/quickstart) | | | |---|---| | What you send | `POST /v1/agents/{id}/execute {prompt, sync: true}` (or `stream: true`) | | What you get back | `{content, workflow_id, task_id, request_id}` or OpenAI SSE chunks | | What your app handles | Holding the connection; keepalives | | How long it takes | One agent run on one connection | | What you pay for | The model tokens the agent uses, within the tier limit; price TBD by owner (TI-31) | | What to check | `X-Execution-Id`, `content` ([Agents quickstart](/agents/quickstart)) | **Before you build:** search first; never hard-code agent ids. `harness-agent` is not callable; loop agents run as a `type: harness` task. ### Start with Agents (async) The answer can arrive later, so the call returns at once with a 202 and you collect the result without holding a connection open. **How to start:** the Agents (sync) request with `sync: false`; store `workflow_id` and `task_id` from the 202; poll `GET /v1/agents/results/{workflowId}/{taskId}` until it stops answering 404 `not_ready`. [Setup details →](/agents/features/async-results) · [Build with Agents →](/agents/features/async-results) | | | |---|---| | What you send | The same, with `sync: false` | | What you get back | 202 `{workflow_id, task_id}`, then the results route | | What your app handles | Polling `GET /v1/agents/results/{workflowId}/{taskId}`; 404 `not_ready` means keep polling | | How long it takes | Returns immediately; the result lands later | | What you pay for | As Agents (sync) | | What to check | 409 `execution_failed`, 422 `empty_completion` ([Async results](/agents/features/async-results)) | **Before you build:** treat 409 `execution_failed` and 422 `empty_completion` as final answers, not as "not ready". ### Start with Workflows A fixed multi-step pipeline across several agents belongs in one CNL workflow, where a parallel block costs only its slowest child. **How to start:** `POST /v1/workflows/execute` with `workflow` (a CNL YAML string), `input` and `format` (`json`, `yaml`, `markdown` or `text`); `POST /v1/workflows/execute-stream` for SSE; `POST /v1/workflows/{id}/execute` for a saved workflow. [Setup details →](/workflows/quickstart) · [Build with Workflows →](/workflows/quickstart) | | | |---|---| | What you send | `POST /v1/workflows/execute {workflow, input, format}` or `execute-stream` | | What you get back | `{success, status: completed or incomplete, workflow_id, final_task_id, content}` or SSE engine events | | What your app handles | Parsing SSE events; the `incomplete` status | | How long it takes | The sum of its sequential tasks; a parallel block costs its slowest child | | What you pay for | Every model call inside the run; price TBD by owner (TI-31) | | What to check | `status`, `final_task_id`, the trace ([Results and traces](/workflows/core-concepts/results-and-traces)) | **Before you build:** search first; never hard-code agent ids. Read `status: incomplete` as a result to inspect, not as a success. ### Start with Missions An agent chooses its own steps, but only inside the tools you allow and the iteration cap you set. **How to start:** over MCP, `plungeai_run_mission` with `goal`, `allowed_tools`, `max_iterations` (1 to 50, default 8), `success_criteria` and `mode` (default `async`); over REST, a `type: harness` task with `agent: one-agent` in `POST /v1/workflows/execute`. [Setup details →](/missions/bounds) · [Build with Missions →](/missions/quickstart) | | | |---|---| | What you send | `plungeai_run_mission {goal, allowed_tools, max_iterations}` or a `type: harness` task | | What you get back | An execution id (async by default), then the result | | What your app handles | Polling status; relaying "AWAITING USER APPROVAL" and calling `plungeai_continue`; a trust-fence "NEEDS APPROVAL" runs only from Studio | | How long it takes | Minutes; resumable legs of about ten minutes | | What you pay for | The model calls in each iteration; price TBD by owner (TI-31) | | What to check | `continuation` in the status output ([Bounds](/missions/bounds)) | **Before you build:** the mission tool has no REST endpoint yet (TI-24). Relay "AWAITING USER APPROVAL" to the user before `plungeai_continue`. ### Start with Scheduling Recurring agents, queries, workflows and heartbeats run on a cron schedule with notifications, with no server of your own. **How to start:** `plungeai_schedule` with `action: create`, `name`, `job_type` (`agent`, `query`, `workflow` or `heartbeat`), `target`, `schedule` (cron), `timezone` and `notify_channel`; then `action: get` and `action: runs`. [Setup details →](/scheduling/quickstart) · [Build with Scheduling →](/scheduling/quickstart) | | | |---|---| | What you send | `plungeai_schedule {action: create, job_type, target, schedule}` | | What you get back | `job_id`; runs on the cron schedule | | What your app handles | Verifying with `get` and `runs`; notification channel ids | | How long it takes | Runs on the schedule | | What you pay for | Each run, priced like the job type it runs (TI-31) | | What to check | Run history ([Runs](/scheduling/runs)) | **Before you build:** MCP only today (TI-24). Verify every job you create with `get` and `runs`. ### Start with the MCP server Your coding agent gets all 20 `plungeai_*` tools from one server, so you write no client code. **How to start:** URL `https://mcp.plungeai.com/v1`; header `Authorization: Bearer $PLUNGE_API_KEY` (an `ozk_` key); transport Streamable HTTP; one command or deeplink per client. [Setup details →](/developer-tools/mcp/quickstart#client-installation) · [Build with the MCP server →](/developer-tools/mcp/plungeai-mcp) | | | |---|---| | What you send | One install command or deeplink ([Overview](/getting-started/overview)) | | What you get back | 20 `plungeai_*` tools, 5 resources, 1 prompt in your client | | What your app handles | Nothing: the client calls the tools | | How long it takes | One install step, then the client is ready | | What you pay for | Whatever the tools run | | What to check | `plungeai_whoami` ([MCP quickstart](/developer-tools/mcp/quickstart)) | **Before you build:** keep the key in your user-level client config or shell, never in a repo file; call `plungeai_whoami` to confirm the connection. ## Have your agent choose an API The `choose-your-plungeai-door` skill teaches your agent the same choice. Download [choose-your-plungeai-door.zip](https://skills.plungeai.com/choose-your-plungeai-door.zip) and add it as a skill. Download [choose-your-plungeai-door.zip](https://skills.plungeai.com/choose-your-plungeai-door.zip) and add it with your Codex skill installer. The skill as a docs page. Source repository: TBD by owner (TI-12). ## Go straight to a quickstart - [Call any model](/models/quickstart) - [Run a tool](/tools/quickstart) - [Run an agent](/agents/quickstart) - [Run a workflow](/workflows/quickstart) - [Run a bounded mission](/missions/quickstart) - [Schedule a job](/scheduling/quickstart) - [Discover capabilities](/discovery/quickstart) Nothing you choose here is saved. # PlungeAI API Pricing Source: https://www.plungeai.com/docs/getting-started/pricing PlungeAI bills model calls from your prepaid wallet, per token, at one of three prices. The numbers below come from `GET /v1/pricing` when these docs are published; the [live table](https://plungeai.com/pricing/) updates over a WebSocket the moment a price changes. ## How billing works - **Wallet, reserve, settle.** Before the upstream call the gateway holds a conservative maximum from your wallet; after it, the hold settles at the true price and the rest is released. A failed call releases the hold: no charge. A near-empty wallet answers `402 insufficient_balance`. - **What you were charged** is in the response: `usage.cost` and the `x-ocean-cost-usd` header carry the price you pay (never the provider cost). `GET /v1/generation?id=…` returns it afterwards, with `pricing_mode`. - **Trial and active accounts.** Every request is metered; only `active` accounts are debited. A `trial` account is priced and logged but never debited, within a $10 a month platform-cost allowance (then `429 insufficient_quota`). An `active` key over its monthly limit answers `429 insufficient_quota`. ## The three prices | Mode | What you pay | `x-ocean-pricing` | |---|---|---| | Posted | Ocean's published price for the model, the same whichever host serves it | `posted` | | Passthrough | The provider cost times your organization markup | `passthrough` | | BYOK | Your own provider key pays the provider; you pay a flat routing fee | `byok` | Every completion carries `x-ocean-pricing` and `x-ocean-provider` (the host that served it). A posted model routes to the cheapest host that holds a key; the price does not change with the host. **The promise:** a posted price never exceeds the model maker's own API list price. **Price changes** reach your bill within 120 seconds of being published. **Aborted streams** settle the tokens that streamed, at the posted price. ## Posted prices Per 1M tokens, USD, published 2026-10-29 01:00 UTC (`published_at` 1793235642982). List = the model maker's own API price. [See all models, live](https://plungeai.com/pricing/). | Model | List in / out | Ocean in / out | Off | |---|---|---|---| | meta-llama/llama-3.3-70b-instruct | $1.04 / $1.04 | $0.125 / $0.40 | 75 % off | | openai/gpt-oss-120b | $0.15 / $0.60 | $0.0463 / $0.2125 | 65 % off | | deepseek/deepseek-v4.1-flash | $0.30 / $1.20 | $0.25 / $0.75 | 33 % off | Every model, including passthrough ones: [Model Catalog](/models/catalog). # API Rate Limits Source: https://www.plungeai.com/docs/getting-started/rate-limits Every key has a tier, copied onto the key when it is created. The tier sets per-minute and per-day windows on the execution planes and the MCP server; the models plane has one per-key limit for every tier. | Surface | Counted on | free | pro | enterprise | |---|---|---|---|---| | Execution planes on `https://api.plungeai.com` (tools, agents, workflows, discovery, traces) | `POST` requests only; catalog `GET`s are not counted; a per-key override is ignored | 30/min · 1,000/day | 100/min · 10,000/day | 300/min · 100,000/day | | MCP server `https://mcp.plungeai.com/v1` | `tools/call` only; a per-key `rate_limit` override set by PlungeAI is honoured | 30/min · 1,000/day | 100/min · 10,000/day | 300/min · 100,000/day | | Models plane (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`) | Every request, per key | 600 req/min | 600 req/min | 600 req/min | | Failed authentication | Per client IP, any tier | 20/min · 500/day | n/a | n/a | A key with no tier counts as free. When you hit a limit you get 429 with Retry-After (seconds): `rate_limited` on the execution planes, `rate_limit_exceeded` on the models plane, and JSON-RPC -32000 with `error.data.retryAfter` on MCP. There are no X-RateLimit-* headers; call `plungeai_whoami` to see your live window. ## Pricing Rate limits are separate from billing: `insufficient_quota`, `spend_cap_exceeded` and `insufficient_balance` come from billing caps, not from these windows. See [Pricing](/getting-started/pricing). ## Need higher limits? Contact PlungeAI support and quote your `x-request-id`. Support contact: TBD by owner (TI-62). # Your first calls Source: https://www.plungeai.com/docs/getting-started/quickstart Four steps from no account to a working agent call, model call and MCP connection. Every command reads its key from an environment variable; no example contains a key. ## 1. Get a key Sign in at https://dashboard.plungeai.com and open **Dashboard → One API → Keys**. Name the key and pick an expiry: never, or 7, 30, 90, 180 or 365 days. The key starts with `ozk_` and is shown exactly once. The same key works for steps 2 to 4. ```bash export PLUNGE_API_KEY=ozk_... # every plane and MCP ``` Which key opens which route is on [Authentication & keys](/getting-started/authentication#the-one-key). ## 2. Call an agent One prompt to `one-agent`, the platform's agent runtime, answered on the same connection: ```python Python import os, requests r = requests.post( "https://api.plungeai.com/v1/agents/one-agent/execute", headers={"Authorization": f"Bearer {os.environ['PLUNGE_API_KEY']}"}, json={"prompt": "In one sentence: what is a content delivery network?", "sync": True, "max_tokens": 60}, ) print(r.json()["content"]) ``` ```typescript TypeScript const r = await fetch('https://api.plungeai.com/v1/agents/one-agent/execute', { method: 'POST', headers: { Authorization: `Bearer ${process.env.PLUNGE_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'In one sentence: what is a content delivery network?', sync: true, max_tokens: 60 }), }) console.log((await r.json()).content) ``` ```java Java var client = java.net.http.HttpClient.newHttpClient(); var request = java.net.http.HttpRequest.newBuilder(java.net.URI.create("https://api.plungeai.com/v1/agents/one-agent/execute")) .header("Authorization", "Bearer " + System.getenv("PLUNGE_API_KEY")) .header("Content-Type", "application/json") .POST(java.net.http.HttpRequest.BodyPublishers.ofString("{\"prompt\":\"In one sentence: what is a content delivery network?\",\"sync\":true,\"max_tokens\":60}")) .build(); System.out.println(client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()).body()); ``` ```bash cURL curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"In one sentence: what is a content delivery network?","sync":true,"max_tokens":60}' ``` The answer is in `content`. Add `"stream": true` to get OpenAI-shaped chunks that end with `data: [DONE]`. ## 3. Call a model The models plane is OpenAI-compatible: point the OpenAI SDK at `https://api.plungeai.com/v1` and pass a `provider/model` slug from `GET /v1/models`. ```python Python import os from openai import OpenAI client = OpenAI(api_key=os.environ["PLUNGE_API_KEY"], base_url="https://api.plungeai.com/v1") resp = client.chat.completions.create( model="anthropic/claude-sonnet-4-6", messages=[{"role": "user", "content": "Reply with exactly the word: pong"}], max_tokens=16, ) print(resp.choices[0].message.content) ``` ```typescript TypeScript import OpenAI from 'openai' const client = new OpenAI({ apiKey: process.env.PLUNGE_API_KEY, baseURL: 'https://api.plungeai.com/v1' }) const resp = await client.chat.completions.create({ model: 'anthropic/claude-sonnet-4-6', messages: [{ role: 'user', content: 'Reply with exactly the word: pong' }], max_tokens: 16, }) console.log(resp.choices[0].message.content) ``` ```java Java var client = java.net.http.HttpClient.newHttpClient(); var request = java.net.http.HttpRequest.newBuilder(java.net.URI.create("https://api.plungeai.com/v1/chat/completions")) .header("Authorization", "Bearer " + System.getenv("PLUNGE_API_KEY")) .header("Content-Type", "application/json") .POST(java.net.http.HttpRequest.BodyPublishers.ofString("{\"model\":\"anthropic/claude-sonnet-4-6\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly the word: pong\"}],\"max_tokens\":16}")) .build(); System.out.println(client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()).body()); ``` ```bash cURL curl -s https://api.plungeai.com/v1/chat/completions \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"model":"anthropic/claude-sonnet-4-6","messages":[{"role":"user","content":"Reply with exactly the word: pong"}],"max_tokens":16}' ``` The `model` field of the response names the model that served the request; after a failover it can differ from the one you asked for. The cURL call is re-run live by the One API guide's example verifier; the SDK snippets send the same request. ## 4. Connect your coding agent One command connects Claude Code to the MCP server and its 20 `plungeai_*` tools: ```bash claude mcp add --transport http plungeai https://mcp.plungeai.com/v1 \ --header "Authorization: Bearer $PLUNGE_API_KEY" --scope user ``` `--scope user` keeps the entry in `~/.claude.json`, outside your repository; never use `--scope project` with a literal key. Run `/mcp` in a session to see `plungeai`, then ask: use plungeai_whoami to confirm my identity. Every other client is on the [MCP quickstart](/developer-tools/mcp/quickstart#client-installation). ## Request ids and traces Every response carries a server-minted `x-request-id`; quote it when you report a problem. To group several calls into one trace, send your own UUID in `x-trace-id` and read the trace with `GET /v1/traces/{id}`: ```bash curl -s "https://api.plungeai.com/v1/traces/$(uuidgen)" -H "Authorization: Bearer $PLUNGE_API_KEY" ``` A trace id you already used on an execution returns `409 duplicate_execution_id`: send a fresh UUID each time. ## Next steps The seven planes and how a request flows. Match your job to a plane. Every field of the agent call. ### Make it a bot A bot is a saved workflow that runs a One Agent loop on a clock, inside a policy you set. Four calls take it from nothing to a scheduled run; each reads the key from the same variable. **1. Create it.** `kind: bot` runs the bot checks on the YAML before anything is stored: ```bash curl -s -X POST https://api.plungeai.com/v1/workflows \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"name":"Daily Brief","kind":"bot","yaml":"workflow:\n name: Daily Brief\n tasks:\n - type: harness\n id: brief\n agent: one-agent\n goal: \"{input}\"\n max_iterations: 6\n allowed_tools: [web_search, web_fetch, task_complete]\n mission: |\n You are Daily Brief. Answer in at most five lines and cite each source URL.\n When your task is finished, call the task_complete tool with your final result.\n"}' ``` The answer is `201` with the saved row under `workflow`. Its `id` is the `` below. **2. Set what it may do and spend.** Ask before it sends anything, never let it pay, cap one run at 25 cents: ```bash curl -s -X PUT https://api.plungeai.com/v1/workflows//permissions \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"classes":{"send":"ask","pay":"hand_off"},"budget_usd_run":0.25}' ``` **3. Run it.** ```bash curl -s -X POST https://api.plungeai.com/v1/workflows//execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"input":"In one line: what is the current Node.js LTS major version? Cite the URL."}' ``` The answer is in `content`. A run that stops on a held action reads `needs_approval`; answer it with `POST /v1/executions/{id}/continue`. **4. Schedule it.** Weekdays at 09:00 Pacific: ```bash curl -s -X POST https://api.plungeai.com/v1/schedules \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"name":"Daily Brief","workflow_id":"","schedule":"0 9 * * 1-5","timezone":"America/Los_Angeles","parameters":{"input":"Top story about Cloudflare Workers today"}}' ``` Add `"deliver": ["slack"]` under `parameters` to send each result to your Slack. The rest of the bot surface (channels, groups, triggers, files, the playbook of keys) starts at [Bots](/bots/overview). # Authentication & keys Source: https://www.plungeai.com/docs/getting-started/authentication Every route except `GET /health` and `GET /v1/openapi.json` needs a key. One `ozk_` key opens every plane; a legacy `sk-ocean-` or `sk-conn-` key opens only its own plane, and a legacy key sent to an execution plane is a `401`, never a silent fallback. These docs use PLUNGE_API_KEY; the PlungeAI skills, the Claude Code plugin and the install page use PLUNGEAI_API_KEY; any name works if your config and shell agree. ## The one key | Prefix | Opens | Variable in these docs | |---|---|---| | `ozk_` | Every plane on `https://api.plungeai.com` (models, tools, agents, workflows, MCP, discovery, traces, connector proxy) and the MCP server `https://mcp.plungeai.com/v1` | `$PLUNGE_API_KEY` | Your key is the **Ocean account key**. Billing follows the account: every metered call on any of your `ozk_` keys (models, embeddings, the connector proxy) is charged to your account's one wallet. The older `sk-ocean-` (models plane) and `sk-conn-` (connector proxy) keys are legacy: still accepted on their own plane while you migrate, no longer created in the Dashboard. ## Which routes accept which key The execution planes (`/v1/discovery`, `/v1/tools`, `/v1/agents`, `/v1/workflows`, `/v1/mcp`, `/v1/traces`) accept only a bearer that starts with `ozk_`; the models plane and the connector proxy accept the same `ozk_` key. The one mistake that answers `401` is a legacy `sk-ocean-` key on an execution plane. Your key on the models plane answers `200`: ```bash # your ozk_ key on the models plane → 200 curl -s -o /dev/null -w '%{http_code}\n' https://api.plungeai.com/v1/models \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` The `401` body is the standard error envelope, and `X-Error-Code: unauthorized` carries the same code as a header. ## Header forms | Header | Accepted on | |---|---| | `Authorization: Bearer ` | Every plane and the MCP server | | `X-API-Key: ` | The execution planes and the MCP server (`ozk_` keys); **not** the models plane (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`), where it answers `401` | ```bash Authorization curl -s "https://api.plungeai.com/v1/tools?limit=1" -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ```bash X-API-Key curl -s "https://api.plungeai.com/v1/tools?limit=1" -H "X-API-Key: $PLUNGE_API_KEY" ``` ## How to obtain each key The key is self-service at **Dashboard → One API → Keys** (https://dashboard.plungeai.com/one-api?tab=keys); legacy `sk-ocean-` and `sk-conn-` keys are listed there read-only, with revoke. Name the key, pick an expiry (never, or 7, 30, 90, 180 or 365 days) and copy it: it is shown exactly once and stored hashed. A new key takes your account's tier. The tab also shows your **Ocean account key** (prefix and last four characters, with **Rotate**): the key the Playground, `ocean-cli` and OceanCode use. The command-line clients fetch it through a browser sign-in, with nothing to paste: `ocean-cli login` and `oceancode login --provider ocean` open `https://dashboard.plungeai.com/api/cli/authorize?port=&state=`; after you sign in, the Dashboard sends a one-time code (valid 120 seconds) to the client listening on `127.0.0.1:`, and the client exchanges it with `POST https://dashboard.plungeai.com/api/cli/token` for the key. ## Expiry, revocation and rotation - **Expiry** is checked on every request; an expired key answers `401` from that moment. - **Revoke** a key from the same Keys list. Keys are cached briefly, so a revoked key can keep working for about two minutes before it answers `401`. If a key leaked, revoke it first and allow for that window. - **Rotate** without downtime: create the new key, update every client, confirm it with `plungeai_whoami`, then revoke the old key. - A lost key cannot be recovered, because only its hash is stored: create a replacement and revoke the old one. ## Key fences Key fences are set by PlungeAI on request. Keys you create in the Dashboard carry no fence: every tool, from any address. On `https://mcp.plungeai.com/v1`, a key fenced to some tools does not see the others in `tools/list`, and a call to one answers JSON-RPC `-32602`; a key fenced to some addresses answers `401` from anywhere else. ## Checking yourself: plungeai_whoami Ask your MCP client to "use plungeai_whoami", or call it directly. It returns the user, the auth type and tier, the key label and the current rate-limit window: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I?"}}}' ``` The fields are on [Who am I](/account-api/identity/whoami). ## CORS The One API answers browser requests from any origin, and the models plane exposes `x-request-id`. Browser code works, but a key in client-side code is a key anyone can read: call PlungeAI from your server. ## Keeping the key safe Treat the key like a password: whoever holds it acts as your account. If a key may have leaked, rotate it now. - Keep the literal key in a user-level client config or an environment variable. A config file inside a repository only references a variable. - Before the first commit in a repository you configured, both commands must print nothing: ```bash git grep -n "ozk_" grep -rn "ozk_" . --exclude-dir=.git --exclude-dir=node_modules ``` - Never put a key in browser code or a client bundle. # How PlungeAI works Source: https://www.plungeai.com/docs/getting-started/concepts The One API is one HTTPS front door, `https://api.plungeai.com`, with one router behind it. The MCP server, `https://mcp.plungeai.com/v1`, gives coding agents the same capabilities as tools. ## The seven planes | Plane | What it does | Key routes | Key | |---|---|---|---| | Models | OpenAI-compatible chat, embeddings and model catalog, with ordered fallback, `sort`, `@preset/`, guardrails and an opt-in response cache | `POST /v1/chat/completions` · `POST /v1/embeddings` · `GET /v1/models` | `ozk_` | | Tools | List tool agents, read a tool's invocation contract, execute one operation behind a trust fence | `GET /v1/tools` · `GET /v1/tools/{id}` · `POST /v1/tools/{id}/execute` | `ozk_` | | Agents | Execute a registry agent from a prompt: sync, async pointer, or OpenAI-shaped SSE | `GET /v1/agents` · `POST /v1/agents/{id}/execute` · `GET /v1/agents/results/{workflowId}/{taskId}` | `ozk_` | | Workflows | Run a multi-agent CNL workflow, inline or saved, with optional SSE | `POST /v1/workflows/execute` · `POST /v1/workflows/execute-stream` · `POST /v1/workflows/{id}/execute` | `ozk_` | | MCP | Outbound MCP runs against catalog servers; coding agents connect to the MCP server | `GET /v1/mcp/tools` · `POST /v1/mcp/runs` | `ozk_` | | Discovery | Search and recommend across the capability registry | `GET /v1/discovery/search` · `POST /v1/discovery/recommend` · `GET /v1/discovery/cards/{type}/{id}` | `ozk_` | | Traces | Read the persisted execution trace for one trace id | `GET /v1/traces/{id}` | `ozk_` | The machine-readable route list is `GET /v1/openapi.json`; this site serves the same operations at [/openapi.json](/openapi.json). ## Positioning Think of the One API as an OpenRouter-compatible models plane plus everything a model router cannot do. The models plane speaks the OpenAI wire format, so an OpenAI or OpenRouter integration moves over with a base-URL swap. The execution planes are the part a router has no answer for: real tools with contracts and a trust fence, registry agents, multi-agent workflows, discovery, MCP, and a persisted trace for every run. ## The MCP endpoint The MCP server is `https://mcp.plungeai.com/v1`: Streamable HTTP, JSON-RPC over `POST`, 20 `plungeai_*` tools, one prompt and five resources plus three resource templates. The pre-2026-09-20 path `/mcp` on the same host still answers with the same handler as a legacy alias; configure `/v1`. Details: [MCP Reference](/mcp-reference/overview). ## How a request flows An execution-plane call runs router → CNL engine → agent: ```mermaid flowchart LR client[Your code] -->|ozk_ key| router[One API router] router -->|service binding| engine[CNL engine] engine --> agent[Registry agent] agent --> memory[(Result and trace)] ``` 1. The router checks the `ozk_` key, mints the `x-request-id`, and adopts your UUID `x-trace-id` or generates a trace id. 2. The CNL engine loads the agent card from the registry and refuses any agent that is not `status:active` before it runs. 3. The agent's result is stored and returned, and the whole run is persisted as trace spans. Every call gets the same outcome vocabulary (`ok`, `needs_input`, `needs_connection`, `needs_api_key`, `needs_approval`, `unavailable`, `error`) and the same trust fence, whether it comes from a workflow, Ocean Studio or your code. ## Tool families | Family | MCP tools | |---|---| | Discover | `plungeai_list_agents`, `plungeai_get_tool_contract`, `plungeai_templates` (list, get) | | Run a tool | `plungeai_execute_tool` | | Run an agent | `plungeai_execute_agent`, `plungeai_run_mission` | | Workflows | `plungeai_list_workflows`, `plungeai_execute_workflow`, `plungeai_workflow`, `plungeai_build_workflow`, `plungeai_templates` (use) | | Runs and results | `plungeai_get_result`, `plungeai_get_workflow_status`, `plungeai_executions`, `plungeai_followup`, `plungeai_continue` | | Chat, memory, skills | `plungeai_chat`, `plungeai_memory`, `plungeai_learn` | | Scheduling | `plungeai_schedule` | | Identity | `plungeai_whoami` | A registry agent is a platform building block in the catalog; "your workflows" are the CNL workflows you saved. ## Live-verified examples Execution-plane examples on this site come from the One API and MCP guides, whose read-only examples were run against the deployed API and are re-run by the guides' example verifier. Models-plane examples use the same key and the same verifier where the guide marks them; the rest are verified against code. Examples that run a model or spend credits say so in the text. ## Where to get help Service health. Error codes and what to do. llms.txt, markdown pages and /agents.md. Every response carries an `x-request-id`: include it when you report a problem. Support contact: TBD by owner (TI-62). # Models Plane Quickstart Source: https://www.plungeai.com/docs/models/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do OpenAI-compatible chat completions with one `ozk_` key and a base-URL swap. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Models Best Practices Source: https://www.plungeai.com/docs/models/best-practices Planned: not available yet. Tracked as TI-74. ## What this will do Request fields, tools and structured output, and what each provider supports. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Evaluating Models Source: https://www.plungeai.com/docs/models/evaluating-models Planned: not available yet. Tracked as TI-77. ## What this will do A guide to comparing models and agents on your own gold set: run the same prompts through several models, score the answers, and read cost and latency next to quality. No evaluation guide exists yet. ## Use this today Send one request across an ordered list of models, sorted by price, latency or throughput. Every model with its provider cost basis. # Routing: models, sort, failover Source: https://www.plungeai.com/docs/models/routing Planned: not available yet. Tracked as TI-74. ## What this will do Ordered fallback with `models[]`, `sort` by price, latency or throughput, and presets. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Migrate to PlungeAI Source: https://www.plungeai.com/docs/models/migrate Planned: not available yet. Tracked as TI-74. ## What this will do Move from OpenRouter, the OpenAI SDK, LiteLLM or the Vercel AI SDK. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Anthropic Messages API Source: https://www.plungeai.com/docs/models/anthropic-messages Partly available: the route is built, and this page is read from the gateway code; it has not yet been re-run against the deployed API. Tracked as TI-78. `POST /v1/messages` speaks the Anthropic Messages API. Change the base URL and the key of an Anthropic client and keep your request and response code. Behind it is the same path as [chat completions](/api-reference/models/create-chat-completion): `models` and `sort` failover, presets, guardrails, and billing to your wallet. ## Drop-in for the Anthropic SDK The SDK adds `/v1/messages` to the base URL and sends your key as `x-api-key`. `Authorization: Bearer` works too. `anthropic-version` is accepted with any value. ```python Python import os import anthropic client = anthropic.Anthropic(base_url='https://api.plungeai.com', api_key=os.environ['PLUNGE_API_KEY']) message = client.messages.create( model='anthropic/claude-haiku-4-5', max_tokens=64, messages=[{'role': 'user', 'content': 'hi'}], ) print(message.content[0].text) ``` ```typescript TypeScript import Anthropic from '@anthropic-ai/sdk' const client = new Anthropic({ baseURL: 'https://api.plungeai.com', apiKey: process.env.PLUNGE_API_KEY }) const message = await client.messages.create({ model: 'anthropic/claude-haiku-4-5', max_tokens: 64, messages: [{ role: 'user', content: 'hi' }], }) console.log(message.content) ``` ```bash cURL curl -s https://api.plungeai.com/v1/messages \ -H "x-api-key: $PLUNGE_API_KEY" \ -H 'anthropic-version: 2023-06-01' \ -H 'Content-Type: application/json' \ -d '{"model":"anthropic/claude-haiku-4-5","max_tokens":64,"messages":[{"role":"user","content":"hi"}]}' ``` The same route is served at `https://api.plungeai.com/api/v1/messages`, the OpenRouter-compatible base. There `fallbacks` takes up to 3 models to try after `model`. ## Anthropic models: your body, unchanged On an `anthropic/*` model the gateway forwards your request body as you sent it and sets only two fields: `model` (the upstream id) and `max_tokens` (the capped value). `cache_control`, `thinking`, documents, `system` blocks and server tools reach Anthropic unchanged. Ocean's own fields (`models`, `fallbacks`, `sort`, `max_cost`) are removed first. ## Every other model: translated Any other slug, such as `openai/gpt-4o-mini`, gets the chat-completions shape: | Messages API | Sent as | |---|---| | `system` | a system message | | `tool_use` blocks | `tool_calls` on the assistant message | | `tool_result` blocks | `tool` messages | | `tools[].input_schema` | `function.parameters` | | `tool_choice` `auto` / `any` / `none` / `tool` | `auto` / `required` / `none` / that function | | `disable_parallel_tool_use` | `parallel_tool_calls: false` | | `stop_sequences` | `stop` | | `metadata.user_id` | `user` | | `thinking.budget_tokens` | `reasoning_effort`: up to 35% of `max_tokens` is `low`, up to 65% `medium`, above that `high` | Blocks with no chat-completions form (thinking, server tools) are not sent to these models. ## Responses You get an Anthropic `message` for every model, built from the completion that served the request. `content` holds `text` and `tool_use` blocks. `model` is the slug that actually answered, which can differ from the one you asked for after a failover. `usage.input_tokens` excludes cache reads and cache writes; those are in `cache_read_input_tokens` and `cache_creation_input_tokens`. The `x-request-id` and `x-ocean-*` response headers are kept. ## Streaming With `stream: true` the events come in Anthropic's order: 1. `message_start` 2. For each content block: `content_block_start`, one or more `content_block_delta` (`text_delta` for text, `input_json_delta` for tool input), `content_block_stop` 3. `message_delta` with `stop_reason` and the final `usage` 4. `message_stop` `ping` events are keepalives. A failure after the response has started ends the stream with an `error` event. The SDK helpers (`client.messages.stream`) read this stream as they read Anthropic's. ## Errors Errors use Anthropic's envelope on both `/v1/messages` and `/api/v1/messages`: ```json {"type":"error","error":{"type":"authentication_error","message":"Invalid API key"},"request_id":"00000000-0000-4000-8000-000000000004"} ``` `error.type` follows the status: `400` `invalid_request_error`, `401` `authentication_error`, `402` `billing_error`, `403` `permission_error`, `404` `not_found_error`, `429` `rate_limit_error`, `504` `timeout_error`, any other `api_error`. ## Not supported - The `anthropic-beta` header is not forwarded, so features behind a beta flag do not switch on. - Responses carry `text` and `tool_use` blocks only; `stop_sequence` is always `null`. - `/v1/messages/count_tokens` and Message Batches are not served. ## Reference Every request field, the response schema, error codes and code samples. # Advanced Models Settings Source: https://www.plungeai.com/docs/models/advanced-settings Planned: not available yet. Tracked as TI-74. ## What this will do Guardrails, the opt-in response cache and BYOK-or-billed routing. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Upgrade from guide 2.0 to 3.0 Source: https://www.plungeai.com/docs/models/migration-guide Planned: not available yet. Tracked as TI-74. ## What this will do What changed between the 2.0 and 3.0 developer guides, for every plane. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Embeddings Quickstart Source: https://www.plungeai.com/docs/models/embeddings Planned: not available yet. Tracked as TI-74. ## What this will do OpenAI-compatible embeddings (`openai/*` models only). This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Models, providers and slugs Source: https://www.plungeai.com/docs/models/catalog Every model the gateway routes, from `GET /v1/pricing` (public, no key) when these docs are published. `GET /v1/models` (with your key) returns the same models in the OpenAI shape, with `pricing.list`, `pricing.ocean` and `pricing.discount_pct`. Live prices: [plungeai.com/pricing](https://plungeai.com/pricing/). ## Model ids A model id is the canonical `author/model` spelling, the same as OpenRouter's (for example `openai/gpt-5`). Send that id as `model`: a posted model then routes to the cheapest host holding a key and bills at the posted price. To pin one host, send that host's upstream slug instead of the canonical id. The request then goes only to that host and bills as passthrough (`x-ocean-pricing: passthrough`). ## All models Per 1M tokens, USD, published 2026-10-29 01:00 UTC (`published_at` 1793235642982). Ocean = the posted price; passthrough models bill at provider cost times your markup. | Model | Provider | Context | List in / out | Ocean in / out | Off | Capabilities | |---|---|---|---|---|---|---| | anthropic/claude-3-5-haiku | anthropic | 200,000 | $0.80 / $4.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-3-5-sonnet | anthropic | 200,000 | $3.00 / $15.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-3-haiku | anthropic | 200,000 | $0.25 / $1.25 | passthrough | — | streaming, tools, vision | | anthropic/claude-fable-5 | anthropic | 1,000,000 | $10.00 / $50.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-fable-5-1 | anthropic | 1,000,000 | $10.00 / $50.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-fable-5.1 | anthropic | 1,000,000 | $10.00 / $50.00 | passthrough | — | streaming, tools | | anthropic/claude-haiku-4-5 | anthropic | 200,000 | $1.00 / $5.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-haiku-4-5-20251001 | anthropic | 200,000 | $1.00 / $5.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-haiku-5-5 | anthropic | 1,000,000 | $0.10 / $0.50 | passthrough | — | streaming, tools, vision | | anthropic/claude-haiku-5.5 | anthropic | 1,000,000 | $0.100000 / $0.50 | passthrough | — | streaming, tools | | anthropic/claude-opus-4 | anthropic | 200,000 | $15.00 / $75.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-4-5-20251101 | anthropic | 200,000 | $5.00 / $25.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-4-6 | anthropic | 1,000,000 | $5.00 / $25.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-4-7 | anthropic | 1,000,000 | $5.00 / $25.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-4-8 | anthropic | 1,000,000 | $5.00 / $25.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-4.1 | anthropic | 200,000 | $15.00 / $75.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-4.5 | anthropic | 200,000 | $5.00 / $25.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-4.6 | anthropic | 1,000,000 | $5.00 / $25.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-4.7-fast | anthropic | 1,000,000 | $30.00 / $150.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-4.8-fast | anthropic | 1,000,000 | $10.00 / $50.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-5 | anthropic | 1,000,000 | $5.00 / $25.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-5-5 | anthropic | 1,000,000 | $4.00 / $20.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-5-fast | anthropic | 1,000,000 | $10.00 / $50.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-opus-5.5 | anthropic | 1,000,000 | $4.00 / $20.00 | passthrough | — | streaming, tools | | anthropic/claude-sonnet-4 | anthropic | 1,000,000 | $3.00 / $15.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-sonnet-4-5 | anthropic | 200,000 | $3.00 / $15.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-sonnet-4-5-20250929 | anthropic | 1,000,000 | $3.00 / $15.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-sonnet-4-6 | anthropic | 1,000,000 | $3.00 / $15.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-sonnet-5 | anthropic | 1,000,000 | $2.00 / $10.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-sonnet-5-5 | anthropic | 1,000,000 | $2.00 / $10.00 | passthrough | — | streaming, tools, vision | | anthropic/claude-sonnet-5.5 | anthropic | 1,000,000 | $2.00 / $10.00 | passthrough | — | streaming, tools | | cerebras/gpt-oss-120b | cerebras | 131,072 | $0.35 / $0.75 | passthrough | — | streaming, tools | | cerebras/llama-3.3-70b | cerebras | 128,000 | $0.85 / $1.20 | passthrough | — | streaming, tools | | cerebras/qwen-3.8-27b | cerebras | 131,072 | $0.990000 / $1.49 | passthrough | — | streaming, tools, vision | | cohere/c4ai-aya-expanse-32b | cohere | 128,000 | $0.50 / $1.50 | passthrough | — | streaming | | cohere/c4ai-aya-vision-32b | cohere | 16,000 | $0.50 / $1.50 | passthrough | — | streaming, vision | | cohere/command-a | cohere | 256,000 | $2.50 / $10.00 | passthrough | — | streaming, tools | | cohere/command-a-03-2025 | cohere | 256,000 | $2.50 / $10.00 | passthrough | — | streaming, tools | | cohere/command-a-plus | cohere | 192,000 | $0.30 / $1.50 | passthrough | — | streaming, tools | | cohere/command-a-plus-05-2026 | cohere | 128,000 | $0.30 / $1.50 | passthrough | — | streaming, tools, vision | | cohere/command-a-reasoning-08-2025 | cohere | 256,000 | $2.50 / $10.00 | passthrough | — | streaming, tools | | cohere/command-a-translate-08-2025 | cohere | 8,192 | $0.50 / $1.50 | passthrough | — | streaming | | cohere/command-a-vision-07-2025 | cohere | 128,000 | $2.50 / $10.00 | passthrough | — | streaming, tools, vision | | cohere/command-r-08-2024 | cohere | 128,000 | $0.15 / $0.60 | passthrough | — | streaming, tools | | cohere/command-r-plus | cohere | 128,000 | $2.50 / $10.00 | passthrough | — | streaming, tools | | cohere/command-r-plus-08-2024 | cohere | 128,000 | $2.50 / $10.00 | passthrough | — | streaming, tools | | cohere/command-r7b-12-2024 | cohere | 128,000 | $0.0375 / $0.15 | passthrough | — | streaming, tools | | cohere/embed-english-light-v3.0 | cohere | 512 | $0.10 / $0.00 | passthrough | — | embedding | | cohere/embed-english-v3.0 | cohere | 512 | $0.10 / $0.00 | passthrough | — | embedding | | cohere/embed-multilingual-light-v3.0 | cohere | 512 | $0.10 / $0.00 | passthrough | — | embedding | | cohere/embed-multilingual-v3.0 | cohere | 512 | $0.10 / $0.00 | passthrough | — | embedding | | cohere/embed-v4.0 | cohere | 128,000 | $0.12 / $0.00 | passthrough | — | embedding | | cohere/embed-v5.0-fast | cohere | 128,000 | $0.08 / $0.00 | passthrough | — | embedding | | cohere/embed-v5.0-pro | cohere | 128,000 | $0.12 / $0.00 | passthrough | — | embedding | | deepinfra/deepseek-ai/DeepSeek-V4.1-Flash | deepinfra | 1,048,576 | $0.20 / $0.60 | passthrough | — | streaming, tools, vision | | deepinfra/meta-llama/Llama-3.3-70B-Instruct-Turbo | deepinfra | 131,072 | $0.10 / $0.32 | passthrough | — | streaming, tools | | deepinfra/openai/gpt-oss-120b | deepinfra | 131,072 | $0.037 / $0.17 | passthrough | — | streaming, tools | | deepseek/deepseek-chat | deepseek | 128,000 | $0.2574 / $1.0287 | passthrough | — | streaming, tools | | deepseek/deepseek-chat-v3-0324 | deepseek | 163,840 | $0.29 / $1.140000 | passthrough | — | streaming, tools | | deepseek/deepseek-chat-v3.1 | deepseek | 163,840 | $0.25 / $0.95 | passthrough | — | streaming, tools | | deepseek/deepseek-flash | deepseek | 1,048,576 | $0.30 / $1.20 | passthrough | — | streaming, tools, vision | | deepseek/deepseek-r1 | deepseek | 163,840 | $0.70 / $2.50 | passthrough | — | streaming, tools | | deepseek/deepseek-r1-0528 | deepseek | 163,840 | $0.50 / $2.150000 | passthrough | — | streaming, tools | | deepseek/deepseek-r1-distill-llama-70b | deepseek | 128,000 | $0.800000 / $0.800000 | passthrough | — | streaming, tools | | deepseek/deepseek-reasoner | deepseek | 128,000 | $0.14 / $0.28 | passthrough | — | streaming, tools | | deepseek/deepseek-v3.1-terminus | deepseek | 163,840 | $0.27 / $1.00 | passthrough | — | streaming, tools | | deepseek/deepseek-v3.2 | deepseek | 131,072 | $0.28 / $0.42 | passthrough | — | streaming, tools | | deepseek/deepseek-v3.2-exp | deepseek | 163,840 | $0.27 / $0.41 | passthrough | — | streaming, tools | | deepseek/deepseek-v4-flash | deepseek | 128,000 | $0.091 / $0.182 | passthrough | — | streaming, tools | | deepseek/deepseek-v4-flash-0731 | deepseek | 1,048,576 | $0.119 / $0.238 | passthrough | — | streaming, tools | | deepseek/deepseek-v4-flash-vision-exp | deepseek | 1,048,576 | $0.215600 / $0.6468 | passthrough | — | streaming, tools | | deepseek/deepseek-v4-pro | deepseek | 1,048,576 | $0.955260 / $1.910520 | passthrough | — | streaming, tools | | deepseek/deepseek-v4-pro-0813 | deepseek | 1,048,576 | $0.66 / $1.980000 | passthrough | — | streaming, tools | | deepseek/deepseek-v4.1-flash | deepseek | 1,048,576 | $0.30 / $1.20 | $0.25 / $0.75 | 33 % off | streaming, tools, vision | | deepseek/deepseek-v4.1-flash-1017 | deepseek | 1,048,576 | $0.15 / $0.60 | passthrough | — | streaming, tools | | fireworks/llama-v3p3-70b | fireworks | 128,000 | $0.90 / $0.90 | passthrough | — | streaming, tools | | google/antigravity-preview-latest | google | 1,048,576 | $0.75 / $3.75 | passthrough | — | streaming, tools, vision | | google/gemini-2.0-flash | google | 1,000,000 | $0.10 / $0.40 | passthrough | — | streaming, tools, vision | | google/gemini-2.5-flash | google | 1,000,000 | $0.30 / $2.50 | passthrough | — | streaming, tools, vision | | google/gemini-2.5-flash-image | google | 32,768 | $0.30 / $2.50 | passthrough | — | streaming, tools, vision | | google/gemini-2.5-flash-lite | google | 1,048,576 | $0.100000 / $0.400000 | passthrough | — | streaming, tools, vision | | google/gemini-2.5-pro | google | 2,000,000 | $1.25 / $10.00 | passthrough | — | streaming, tools, vision | | google/gemini-2.5-pro-preview | google | 1,048,576 | $1.25 / $10.00 | passthrough | — | streaming, tools, vision | | google/gemini-2.5-pro-preview-05-06 | google | 1,048,576 | $1.25 / $10.00 | passthrough | — | streaming, tools, vision | | google/gemini-3-flash-preview | google | 1,000,000 | $0.50 / $3.00 | passthrough | — | streaming, tools, vision | | google/gemini-3-pro-image | google | 65,536 | $2.00 / $12.00 | passthrough | — | streaming, tools, vision | | google/gemini-3-pro-image-preview | google | 65,536 | $2.00 / $12.00 | passthrough | — | streaming, tools, vision | | google/gemini-3.1-flash-image | google | 131,072 | $0.50 / $3.00 | passthrough | — | streaming, tools, vision | | google/gemini-3.1-flash-image-preview | google | 131,072 | $0.50 / $3.00 | passthrough | — | streaming, tools, vision | | google/gemini-3.1-flash-lite | google | 1,000,000 | $0.25 / $1.50 | passthrough | — | streaming, tools, vision | | google/gemini-3.1-flash-lite-image | google | 65,536 | $0.25 / $1.50 | passthrough | — | streaming, tools, vision | | google/gemini-3.1-flash-lite-preview | google | 1,048,576 | $0.25 / $1.50 | passthrough | — | streaming, tools, vision | | google/gemini-3.1-pro | google | 2,000,000 | $2.00 / $12.00 | passthrough | — | streaming, tools, vision | | google/gemini-3.1-pro-preview | google | 1,048,576 | $2.00 / $12.00 | passthrough | — | streaming, tools, vision | | google/gemini-3.1-pro-preview-customtools | google | 1,048,756 | $2.00 / $12.00 | passthrough | — | streaming, tools, vision | | google/gemini-3.5-flash | google | 1,000,000 | $1.50 / $9.00 | passthrough | — | streaming, tools, vision | | google/gemini-3.5-flash-lite | google | 1,048,576 | $0.30 / $2.50 | passthrough | — | streaming, tools, vision | | google/gemini-3.6-flash | google | 1,048,576 | $0.75 / $3.75 | passthrough | — | streaming, tools, vision | | google/gemini-3.7-flash | google | 1,048,576 | $0.75 / $3.75 | passthrough | — | streaming, tools, vision | | google/gemini-3.8-flash | google | 1,048,576 | $0.825000 / $4.125 | passthrough | — | streaming, tools | | google/gemini-embedding-001 | google | 2,048 | $0.15 / $0.00 | passthrough | — | embedding | | google/gemini-embedding-2 | google | 8,192 | $0.20 / $0.00 | passthrough | — | embedding | | google/gemini-flash-latest | google | 1,048,576 | $0.75 / $3.75 | passthrough | — | streaming, tools, vision | | google/gemini-flash-lite-latest | google | 1,048,576 | $0.30 / $2.50 | passthrough | — | streaming, tools, vision | | google/gemini-nano-banana-2.1 | google | 65,536 | $1.50 / $7.50 | passthrough | — | streaming, tools, vision | | google/gemini-pro-latest | google | 1,048,576 | $2.00 / $12.00 | passthrough | — | streaming, tools, vision | | google/gemini-robotics-er-2-preview | google | 131,072 | $1.00 / $5.00 | passthrough | — | streaming, tools, vision | | google/gemma-2-27b-it | google | 8,192 | $0.65 / $0.65 | passthrough | — | streaming, tools | | google/gemma-3-12b-it | google | 131,072 | $0.050000 / $0.15 | passthrough | — | streaming, tools | | google/gemma-3-27b-it | google | 131,072 | $0.08 / $0.35 | passthrough | — | streaming, tools | | google/gemma-3-4b-it | google | 131,072 | $0.050000 / $0.100000 | passthrough | — | streaming, tools | | google/gemma-3n-e4b-it | google | 32,768 | $0.06 / $0.12 | passthrough | — | streaming, tools | | google/gemma-4-26b-a4b-it | google | 262,144 | $0.0765 / $0.255 | passthrough | — | streaming, tools | | google/gemma-4-31b-it | google | 262,144 | $0.200000 / $0.400000 | passthrough | — | streaming, tools | | groq/openai/gpt-oss-120b | groq | 131,072 | $0.15 / $0.60 | passthrough | — | streaming, tools | | groq/openai/gpt-oss-20b | groq | 131,072 | $0.075 / $0.30 | passthrough | — | streaming, tools | | groq/qwen/qwen3.8-27b | groq | 131,072 | $0.80 / $4.00 | passthrough | — | streaming, tools, vision | | meta-llama/llama-3.3-70b-instruct | meta-llama | 131,072 | $1.04 / $1.04 | $0.125 / $0.40 | 75 % off | streaming, tools | | mistral/codestral-2508 | mistral | 256,000 | $0.30 / $0.900000 | passthrough | — | streaming, tools | | mistral/codestral-embed | mistral | 8,192 | $0.15 / $0.00 | passthrough | — | embedding | | mistral/codestral-embed-2505 | mistral | 8,192 | $0.15 / $0.00 | passthrough | — | embedding | | mistral/codestral-latest | mistral | 256,000 | $0.30 / $0.90 | passthrough | — | streaming, tools | | mistral/devstral-2512 | mistral | 262,144 | $0.400000 / $2.00 | passthrough | — | streaming, tools | | mistral/ministral-14b-2512 | mistral | 262,144 | $0.200000 / $0.200000 | passthrough | — | streaming, tools | | mistral/ministral-14b-latest | mistral | 262,144 | $0.20 / $0.20 | passthrough | — | streaming, tools, vision | | mistral/ministral-3b-2512 | mistral | 131,072 | $0.100000 / $0.100000 | passthrough | — | streaming, tools | | mistral/ministral-3b-latest | mistral | 262,144 | $0.10 / $0.10 | passthrough | — | streaming, tools, vision | | mistral/ministral-8b | mistral | 128,000 | $0.11 / $0.11 | passthrough | — | streaming, tools | | mistral/ministral-8b-2512 | mistral | 262,144 | $0.15 / $0.15 | passthrough | — | streaming, tools | | mistral/ministral-8b-latest | mistral | 262,144 | $0.15 / $0.15 | passthrough | — | streaming, tools, vision | | mistral/mistral-embed | mistral | 8,192 | $0.10 / $0.00 | passthrough | — | embedding | | mistral/mistral-embed-2312 | mistral | 8,192 | $0.10 / $0.00 | passthrough | — | embedding | | mistral/mistral-large | mistral | 128,000 | $2.00 / $6.00 | passthrough | — | streaming, tools | | mistral/mistral-large-2407 | mistral | 131,072 | $2.00 / $6.00 | passthrough | — | streaming, tools | | mistral/mistral-large-2512 | mistral | 262,144 | $0.50 / $1.50 | passthrough | — | streaming, tools | | mistral/mistral-large-4-0 | mistral | 524,288 | $0.680000 / $2.09 | passthrough | — | streaming, tools | | mistral/mistral-large-latest | mistral | 128,000 | $0.50 / $1.50 | passthrough | — | streaming, tools | | mistral/mistral-medium-3 | mistral | 131,072 | $0.400000 / $2.00 | passthrough | — | streaming, tools | | mistral/mistral-medium-3-5 | mistral | 262,144 | $1.50 / $7.50 | passthrough | — | streaming, tools | | mistral/mistral-medium-3.1 | mistral | 131,072 | $0.400000 / $2.00 | passthrough | — | streaming, tools | | mistral/mistral-medium-latest | mistral | 262,144 | $1.50 / $7.50 | passthrough | — | streaming, tools, vision | | mistral/mistral-nemo | mistral | 131,072 | $0.02 / $0.03 | passthrough | — | streaming, tools | | mistral/mistral-saba | mistral | 32,768 | $0.200000 / $0.60 | passthrough | — | streaming, tools | | mistral/mistral-small-24b-instruct-2501 | mistral | 32,768 | $0.050000 / $0.08 | passthrough | — | streaming, tools | | mistral/mistral-small-2603 | mistral | 262,144 | $0.15 / $0.60 | passthrough | — | streaming, tools | | mistral/mistral-small-3.1-24b-instruct | mistral | 128,000 | $0.351 / $0.555000 | passthrough | — | streaming, tools | | mistral/mistral-small-3.2-24b-instruct | mistral | 128,000 | $0.075 / $0.200000 | passthrough | — | streaming, tools | | mistral/mistral-small-latest | mistral | 128,000 | $0.15 / $0.60 | passthrough | — | streaming, tools | | mistral/mixtral-8x22b-instruct | mistral | 65,536 | $2.00 / $6.00 | passthrough | — | streaming, tools | | mistral/voxtral-small-24b-2507 | mistral | 32,000 | $0.100000 / $0.30 | passthrough | — | streaming, tools | | mistral/voxtral-small-2507 | mistral | 32,768 | $0.10 / $0.40 | passthrough | — | streaming, tools | | mistral/voxtral-small-latest | mistral | 32,768 | $0.10 / $0.40 | passthrough | — | streaming, tools | | mistral/zai-glm-5-3 | mistral | 1,048,576 | $1.40 / $4.40 | passthrough | — | streaming, tools | | mistral/zai-glm-latest | mistral | 1,048,576 | $1.40 / $4.40 | passthrough | — | streaming, tools | | moonshot/kimi-k2 | moonshot | 128,000 | $0.60 / $2.50 | passthrough | — | streaming, tools, vision | | moonshot/kimi-k2-0905 | moonshot | 262,144 | $0.60 / $2.50 | passthrough | — | streaming, tools, vision | | moonshot/kimi-k2-thinking | moonshot | 262,144 | $0.60 / $2.50 | passthrough | — | streaming, tools, vision | | moonshot/kimi-k2.5 | moonshot | 256,000 | $0.60 / $3.00 | passthrough | — | streaming, tools, vision | | moonshot/kimi-k2.6 | moonshot | 256,000 | $0.95 / $4.00 | passthrough | — | streaming, tools, vision | | moonshot/kimi-k2.7-code | moonshot | 262,144 | $0.72 / $3.49 | passthrough | — | streaming, tools, vision | | moonshot/kimi-k2.7-code-highspeed | moonshot | 262,144 | $1.90 / $8.00 | passthrough | — | streaming, tools, vision | | moonshot/kimi-k3 | moonshot | 1,048,576 | $3.00 / $15.00 | passthrough | — | streaming, tools, vision | | moonshot/kimi-k3.1-pro | moonshot | 1,048,576 | $2.00 / $10.00 | passthrough | — | streaming, tools | | openai/gpt-3.5-turbo | openai | 16,385 | $0.50 / $1.50 | passthrough | — | streaming, tools | | openai/gpt-3.5-turbo-0613 | openai | 4,095 | $1.00 / $2.00 | passthrough | — | streaming, tools | | openai/gpt-3.5-turbo-16k | openai | 16,385 | $3.00 / $4.00 | passthrough | — | streaming, tools | | openai/gpt-3.5-turbo-instruct | openai | 4,095 | $1.50 / $2.00 | passthrough | — | streaming, tools | | openai/gpt-4 | openai | 8,191 | $30.00 / $60.00 | passthrough | — | streaming, tools | | openai/gpt-4-turbo | openai | 128,000 | $10.00 / $30.00 | passthrough | — | streaming, tools, vision | | openai/gpt-4-turbo-preview | openai | 128,000 | $10.00 / $30.00 | passthrough | — | streaming, tools, vision | | openai/gpt-4.1 | openai | 1,000,000 | $2.00 / $8.00 | passthrough | — | streaming, tools | | openai/gpt-4.1-mini | openai | 1,000,000 | $0.40 / $1.60 | passthrough | — | streaming, tools | | openai/gpt-4.1-nano | openai | 1,047,576 | $0.100000 / $0.400000 | passthrough | — | streaming, tools | | openai/gpt-4o | openai | 128,000 | $2.50 / $10.00 | passthrough | — | streaming, tools, vision | | openai/gpt-4o-2024-05-13 | openai | 128,000 | $5.00 / $15.00 | passthrough | — | streaming, tools, vision | | openai/gpt-4o-2024-08-06 | openai | 128,000 | $2.50 / $10.00 | passthrough | — | streaming, tools, vision | | openai/gpt-4o-2024-11-20 | openai | 128,000 | $2.50 / $10.00 | passthrough | — | streaming, tools, vision | | openai/gpt-4o-mini | openai | 128,000 | $0.15 / $0.60 | passthrough | — | streaming, tools, vision | | openai/gpt-4o-mini-2024-07-18 | openai | 128,000 | $0.15 / $0.60 | passthrough | — | streaming, tools, vision | | openai/gpt-4o-mini-search-preview | openai | 128,000 | $0.15 / $0.60 | passthrough | — | streaming, tools, vision | | openai/gpt-4o-search-preview | openai | 128,000 | $2.50 / $10.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5 | openai | 400,000 | $1.25 / $10.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5-chat | openai | 128,000 | $1.25 / $10.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5-image | openai | 400,000 | $10.00 / $10.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5-image-mini | openai | 400,000 | $2.50 / $2.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5-mini | openai | 400,000 | $0.25 / $2.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5-nano | openai | 400,000 | $0.050000 / $0.400000 | passthrough | — | streaming, tools, vision | | openai/gpt-5-pro | openai | 400,000 | $15.00 / $120.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5-search-api | openai | 400,000 | $1.25 / $10.00 | passthrough | — | streaming, tools | | openai/gpt-5.1 | openai | 400,000 | $1.25 / $10.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.1-chat | openai | 128,000 | $1.25 / $10.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.2 | openai | 400,000 | $1.75 / $14.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.2-chat | openai | 128,000 | $1.75 / $14.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.2-pro | openai | 400,000 | $21.00 / $168.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.3-chat | openai | 128,000 | $1.75 / $14.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.3-codex | openai | 400,000 | $1.75 / $14.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.4 | openai | 400,000 | $2.50 / $15.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.4-image-2 | openai | 272,000 | $8.00 / $15.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.4-mini | openai | 400,000 | $0.75 / $4.50 | passthrough | — | streaming, tools, vision | | openai/gpt-5.4-nano | openai | 400,000 | $0.200000 / $1.25 | passthrough | — | streaming, tools, vision | | openai/gpt-5.4-pro | openai | 1,050,000 | $30.00 / $180.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.5 | openai | 400,000 | $5.00 / $30.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.5-pro | openai | 1,050,000 | $30.00 / $180.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.6-luna | openai | 1,000,000 | $0.200000 / $1.20 | passthrough | — | streaming, tools, vision | | openai/gpt-5.6-luna-pro | openai | 1,050,000 | $0.200000 / $1.20 | passthrough | — | streaming, tools, vision | | openai/gpt-5.6-sol | openai | 1,000,000 | $4.00 / $20.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.6-sol-pro | openai | 1,050,000 | $4.00 / $20.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.6-terra | openai | 1,000,000 | $2.00 / $12.00 | passthrough | — | streaming, tools, vision | | openai/gpt-5.6-terra-pro | openai | 1,050,000 | $2.00 / $12.00 | passthrough | — | streaming, tools, vision | | openai/gpt-6-astra | openai | 1,050,000 | $10.00 / $50.00 | passthrough | — | streaming, tools | | openai/gpt-6-astra-pro | openai | 1,050,000 | $10.00 / $50.00 | passthrough | — | streaming, tools | | openai/gpt-6-luna | openai | 1,050,000 | $0.100000 / $0.50 | passthrough | — | streaming, tools | | openai/gpt-6-luna-pro | openai | 1,050,000 | $0.100000 / $0.50 | passthrough | — | streaming, tools | | openai/gpt-6-sol | openai | 1,050,000 | $2.00 / $10.00 | passthrough | — | streaming, tools | | openai/gpt-6-sol-pro | openai | 1,050,000 | $2.00 / $10.00 | passthrough | — | streaming, tools | | openai/gpt-6.1-sol | openai | 1,050,000 | $2.00 / $10.00 | passthrough | — | streaming, tools | | openai/gpt-6.1-sol-pro | openai | 1,050,000 | $2.00 / $10.00 | passthrough | — | streaming, tools | | openai/gpt-6.1-terra | openai | 1,050,000 | $1.00 / $5.00 | passthrough | — | streaming, tools | | openai/gpt-6.1-terra-pro | openai | 1,050,000 | $1.00 / $5.00 | passthrough | — | streaming, tools | | openai/gpt-audio | openai | 128,000 | $2.50 / $10.00 | passthrough | — | streaming, tools | | openai/gpt-audio-mini | openai | 128,000 | $0.60 / $2.40 | passthrough | — | streaming, tools | | openai/gpt-chat-latest | openai | 400,000 | $5.00 / $30.00 | passthrough | — | streaming, tools | | openai/gpt-oss-120b | openai | 131,072 | $0.15 / $0.60 | $0.0463 / $0.2125 | 65 % off | streaming, tools | | openai/o1 | openai | 200,000 | $15.00 / $60.00 | passthrough | — | streaming, tools | | openai/o1-mini | openai | 128,000 | $1.10 / $4.40 | passthrough | — | streaming | | openai/o1-pro | openai | 200,000 | $150.00 / $600.00 | passthrough | — | streaming, tools | | openai/o3 | openai | 200,000 | $2.00 / $8.00 | passthrough | — | streaming, tools | | openai/o3-mini | openai | 200,000 | $1.10 / $4.40 | passthrough | — | streaming, tools | | openai/o3-mini-high | openai | 200,000 | $1.10 / $4.40 | passthrough | — | streaming, tools | | openai/o3-pro | openai | 200,000 | $20.00 / $80.00 | passthrough | — | streaming, tools | | openai/o4-mini | openai | 200,000 | $1.10 / $4.40 | passthrough | — | streaming, tools | | openai/o4-mini-high | openai | 200,000 | $1.10 / $4.40 | passthrough | — | streaming, tools | | openai/text-embedding-3-large | openai | 8,191 | $0.13 / $0.00 | passthrough | — | embedding | | openai/text-embedding-3-small | openai | 8,191 | $0.02 / $0.00 | passthrough | — | embedding | | openrouter/ai21/jamba-large-1.7 | openrouter | 256,000 | $2.11 / $8.44 | passthrough | — | streaming, tools | | openrouter/aion-labs/aion-2.0 | openrouter | 131,072 | $0.844000 / $1.688000 | passthrough | — | streaming, tools | | openrouter/aion-labs/aion-rp-llama-3.1-8b | openrouter | 32,768 | $0.844000 / $1.688000 | passthrough | — | streaming, tools | | openrouter/allenai/olmo-3-32b-think | openrouter | 65,536 | $0.158250 / $0.5275 | passthrough | — | streaming, tools | | openrouter/amazon/nova-2-lite-v1 | openrouter | 1,000,000 | $0.316500 / $2.637500 | passthrough | — | streaming, tools | | openrouter/amazon/nova-lite-v1 | openrouter | 300,000 | $0.0633 / $0.2532 | passthrough | — | streaming, tools | | openrouter/amazon/nova-micro-v1 | openrouter | 128,000 | $0.036925 / $0.1477 | passthrough | — | streaming, tools | | openrouter/amazon/nova-premier-v1 | openrouter | 1,000,000 | $2.637500 / $13.1875 | passthrough | — | streaming, tools | | openrouter/amazon/nova-pro-v1 | openrouter | 300,000 | $0.844000 / $3.376000 | passthrough | — | streaming, tools | | openrouter/anthracite-org/magnum-v4-72b | openrouter | 32,768 | $3.165 / $5.275000 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-3-haiku | openrouter | 200,000 | $0.26375 / $1.318750 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-haiku-4.5 | openrouter | 200,000 | $1.055 / $5.275000 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-opus-4 | openrouter | 200,000 | $15.825 / $79.125 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-opus-4.1 | openrouter | 200,000 | $15.825 / $79.125 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-opus-4.5 | openrouter | 200,000 | $5.275000 / $26.375 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-opus-4.6 | openrouter | 1,000,000 | $5.275000 / $26.375 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-opus-4.7 | openrouter | 1,000,000 | $5.275000 / $26.375 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-opus-4.7-fast | openrouter | 1,000,000 | $31.65 / $158.25 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-sonnet-4 | openrouter | 1,000,000 | $3.165 / $15.825 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-sonnet-4.5 | openrouter | 1,000,000 | $3.165 / $15.825 | passthrough | — | streaming, tools | | openrouter/anthropic/claude-sonnet-4.6 | openrouter | 1,000,000 | $3.165 / $15.825 | passthrough | — | streaming, tools | | openrouter/arcee-ai/coder-large | openrouter | 32,768 | $0.5275 / $0.844000 | passthrough | — | streaming, tools | | openrouter/arcee-ai/trinity-large-thinking | openrouter | 262,144 | $0.26375 / $0.844000 | passthrough | — | streaming, tools | | openrouter/arcee-ai/virtuoso-large | openrouter | 131,072 | $0.79125 / $1.266000 | passthrough | — | streaming, tools | | openrouter/baidu/ernie-4.5-vl-424b-a47b | openrouter | 131,072 | $0.443100 / $1.318750 | passthrough | — | streaming, tools | | openrouter/bytedance-seed/seed-1.6 | openrouter | 262,144 | $0.26375 / $2.11 | passthrough | — | streaming, tools | | openrouter/bytedance-seed/seed-1.6-flash | openrouter | 262,144 | $0.079125 / $0.316500 | passthrough | — | streaming, tools | | openrouter/bytedance-seed/seed-2.0-lite | openrouter | 262,144 | $0.26375 / $2.11 | passthrough | — | streaming, tools | | openrouter/bytedance-seed/seed-2.0-mini | openrouter | 262,144 | $0.105500 / $0.422000 | passthrough | — | streaming, tools | | openrouter/bytedance/ui-tars-1.5-7b | openrouter | 128,000 | $0.105500 / $0.211000 | passthrough | — | streaming, tools | | openrouter/cohere/command-a | openrouter | 256,000 | $2.637500 / $10.550000 | passthrough | — | streaming, tools | | openrouter/cohere/command-r-08-2024 | openrouter | 128,000 | $0.158250 / $0.633000 | passthrough | — | streaming, tools | | openrouter/cohere/command-r-plus-08-2024 | openrouter | 128,000 | $2.637500 / $10.550000 | passthrough | — | streaming, tools | | openrouter/cohere/command-r7b-12-2024 | openrouter | 128,000 | $0.039562 / $0.158250 | passthrough | — | streaming, tools | | openrouter/deepcogito/cogito-v2.1-671b | openrouter | 128,000 | $1.318750 / $1.318750 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-chat | openrouter | 131,072 | $0.211211 / $0.844105 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-chat-v3-0324 | openrouter | 163,840 | $0.2532 / $0.949500 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-chat-v3.1 | openrouter | 163,840 | $0.221550 / $0.833450 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-r1 | openrouter | 163,840 | $0.738500 / $2.637500 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-r1-0528 | openrouter | 163,840 | $0.5275 / $2.26825 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-r1-distill-llama-70b | openrouter | 128,000 | $0.844000 / $0.844000 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-v3.1-terminus | openrouter | 163,840 | $0.28485 / $1.002250 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-v3.2 | openrouter | 131,072 | $0.226297 / $0.339446 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-v3.2-exp | openrouter | 163,840 | $0.28485 / $0.432550 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-v4-flash | openrouter | 1,048,576 | $0.088620 / $0.177240 | passthrough | — | streaming, tools | | openrouter/deepseek/deepseek-v4-pro | openrouter | 1,048,576 | $0.458925 / $0.917850 | passthrough | — | streaming, tools | | openrouter/google/gemini-2.5-flash | openrouter | 1,048,576 | $0.316500 / $2.637500 | passthrough | — | streaming, tools | | openrouter/google/gemini-2.5-flash-image | openrouter | 32,768 | $0.316500 / $2.637500 | passthrough | — | streaming, tools | | openrouter/google/gemini-2.5-flash-lite | openrouter | 1,048,576 | $0.105500 / $0.422000 | passthrough | — | streaming, tools | | openrouter/google/gemini-2.5-pro | openrouter | 1,048,576 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/google/gemini-2.5-pro-preview | openrouter | 1,048,576 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/google/gemini-2.5-pro-preview-05-06 | openrouter | 1,048,576 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/google/gemini-3-flash-preview | openrouter | 1,048,576 | $0.5275 / $3.165 | passthrough | — | streaming, tools | | openrouter/google/gemini-3-pro-image-preview | openrouter | 65,536 | $2.11 / $12.66 | passthrough | — | streaming, tools | | openrouter/google/gemini-3.1-flash-image-preview | openrouter | 131,072 | $0.5275 / $3.165 | passthrough | — | streaming, tools | | openrouter/google/gemini-3.1-flash-lite | openrouter | 1,048,576 | $0.26375 / $1.5825 | passthrough | — | streaming, tools | | openrouter/google/gemini-3.1-flash-lite-preview | openrouter | 1,048,576 | $0.26375 / $1.5825 | passthrough | — | streaming, tools | | openrouter/google/gemini-3.1-pro-preview | openrouter | 1,048,576 | $2.11 / $12.66 | passthrough | — | streaming, tools | | openrouter/google/gemini-3.1-pro-preview-customtools | openrouter | 1,048,756 | $2.11 / $12.66 | passthrough | — | streaming, tools | | openrouter/google/gemini-3.5-flash | openrouter | 1,048,576 | $1.5825 / $9.495 | passthrough | — | streaming, tools | | openrouter/google/gemma-2-27b-it | openrouter | 8,192 | $0.68575 / $0.68575 | passthrough | — | streaming, tools | | openrouter/google/gemma-3-12b-it | openrouter | 131,072 | $0.052750 / $0.158250 | passthrough | — | streaming, tools | | openrouter/google/gemma-3-27b-it | openrouter | 131,072 | $0.0844 / $0.1688 | passthrough | — | streaming, tools | | openrouter/google/gemma-3-4b-it | openrouter | 131,072 | $0.052750 / $0.105500 | passthrough | — | streaming, tools | | openrouter/google/gemma-3n-e4b-it | openrouter | 32,768 | $0.0633 / $0.1266 | passthrough | — | streaming, tools | | openrouter/google/gemma-4-26b-a4b-it | openrouter | 262,144 | $0.0633 / $0.34815 | passthrough | — | streaming, tools | | openrouter/google/gemma-4-31b-it | openrouter | 262,144 | $0.1266 / $0.369250 | passthrough | — | streaming, tools | | openrouter/gryphe/mythomax-l2-13b | openrouter | 4,096 | $0.0633 / $0.0633 | passthrough | — | streaming, tools | | openrouter/ibm-granite/granite-4.0-h-micro | openrouter | 131,000 | $0.017935 / $0.11816 | passthrough | — | streaming, tools | | openrouter/ibm-granite/granite-4.1-8b | openrouter | 131,072 | $0.052750 / $0.105500 | passthrough | — | streaming, tools | | openrouter/inception/mercury-2 | openrouter | 128,000 | $0.26375 / $0.79125 | passthrough | — | streaming, tools | | openrouter/inclusionai/ling-2.6-1t | openrouter | 262,144 | $0.079125 / $0.659375 | passthrough | — | streaming, tools | | openrouter/inclusionai/ling-2.6-flash | openrouter | 262,144 | $0.01055 / $0.03165 | passthrough | — | streaming, tools | | openrouter/inclusionai/ring-2.6-1t | openrouter | 262,144 | $0.079125 / $0.659375 | passthrough | — | streaming, tools | | openrouter/inflection/inflection-3-pi | openrouter | 8,000 | $2.637500 / $10.550000 | passthrough | — | streaming, tools | | openrouter/inflection/inflection-3-productivity | openrouter | 8,000 | $2.637500 / $10.550000 | passthrough | — | streaming, tools | | openrouter/kwaipilot/kat-coder-pro-v2 | openrouter | 256,000 | $0.316500 / $1.266000 | passthrough | — | streaming, tools | | openrouter/mancer/weaver | openrouter | 8,000 | $0.79125 / $1.055 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-3-8b-instruct | openrouter | 8,192 | $0.1477 / $0.1477 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-3.1-70b-instruct | openrouter | 131,072 | $0.422000 / $0.422000 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-3.1-8b-instruct | openrouter | 131,072 | $0.0211 / $0.03165 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-3.2-11b-vision-instruct | openrouter | 131,072 | $0.363975 / $0.363975 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-3.2-1b-instruct | openrouter | 131,072 | $0.028485 / $0.212055 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-3.2-3b-instruct | openrouter | 131,072 | $0.052750 / $0.34815 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-3.3-70b-instruct | openrouter | 131,072 | $0.105500 / $0.3376 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-4-maverick | openrouter | 1,048,576 | $0.158250 / $0.633000 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-4-scout | openrouter | 10,000,000 | $0.105500 / $0.316500 | passthrough | — | streaming, tools | | openrouter/meta-llama/llama-guard-4-12b | openrouter | 163,840 | $0.189900 / $0.189900 | passthrough | — | streaming, tools | | openrouter/microsoft/phi-4 | openrouter | 16,384 | $0.07385 / $0.1477 | passthrough | — | streaming, tools | | openrouter/microsoft/wizardlm-2-8x22b | openrouter | 65,536 | $0.654100 / $0.654100 | passthrough | — | streaming, tools | | openrouter/minimax/minimax-01 | openrouter | 1,000,192 | $0.211000 / $1.1605 | passthrough | — | streaming, tools | | openrouter/minimax/minimax-m1 | openrouter | 1,000,000 | $0.422000 / $2.321 | passthrough | — | streaming, tools | | openrouter/minimax/minimax-m2 | openrouter | 204,800 | $0.269025 / $1.0761 | passthrough | — | streaming, tools | | openrouter/minimax/minimax-m2-her | openrouter | 65,536 | $0.316500 / $1.266000 | passthrough | — | streaming, tools | | openrouter/minimax/minimax-m2.1 | openrouter | 204,800 | $0.316500 / $1.266000 | passthrough | — | streaming, tools | | openrouter/minimax/minimax-m2.5 | openrouter | 204,800 | $0.158250 / $0.949500 | passthrough | — | streaming, tools | | openrouter/minimax/minimax-m2.7 | openrouter | 204,800 | $0.2532 / $1.0128 | passthrough | — | streaming, tools | | openrouter/mistralai/codestral-2508 | openrouter | 256,000 | $0.316500 / $0.949500 | passthrough | — | streaming, tools | | openrouter/mistralai/devstral-2512 | openrouter | 262,144 | $0.422000 / $2.11 | passthrough | — | streaming, tools | | openrouter/mistralai/ministral-14b-2512 | openrouter | 262,144 | $0.211000 / $0.211000 | passthrough | — | streaming, tools | | openrouter/mistralai/ministral-3b-2512 | openrouter | 131,072 | $0.105500 / $0.105500 | passthrough | — | streaming, tools | | openrouter/mistralai/ministral-8b-2512 | openrouter | 262,144 | $0.158250 / $0.158250 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-large | openrouter | 128,000 | $2.11 / $6.33 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-large-2407 | openrouter | 131,072 | $2.11 / $6.33 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-large-2512 | openrouter | 262,144 | $0.5275 / $1.5825 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-medium-3 | openrouter | 131,072 | $0.422000 / $2.11 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-medium-3-5 | openrouter | 262,144 | $1.5825 / $7.9125 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-medium-3.1 | openrouter | 131,072 | $0.422000 / $2.11 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-nemo | openrouter | 131,072 | $0.0211 / $0.03165 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-saba | openrouter | 32,768 | $0.211000 / $0.633000 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-small-24b-instruct-2501 | openrouter | 32,768 | $0.052750 / $0.0844 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-small-2603 | openrouter | 262,144 | $0.158250 / $0.633000 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-small-3.1-24b-instruct | openrouter | 128,000 | $0.370305 / $0.585525 | passthrough | — | streaming, tools | | openrouter/mistralai/mistral-small-3.2-24b-instruct | openrouter | 128,000 | $0.079125 / $0.211000 | passthrough | — | streaming, tools | | openrouter/mistralai/mixtral-8x22b-instruct | openrouter | 65,536 | $2.11 / $6.33 | passthrough | — | streaming, tools | | openrouter/mistralai/voxtral-small-24b-2507 | openrouter | 32,000 | $0.105500 / $0.316500 | passthrough | — | streaming, tools | | openrouter/moonshotai/kimi-k2 | openrouter | 131,072 | $0.60135 / $2.4265 | passthrough | — | streaming, tools | | openrouter/moonshotai/kimi-k2-0905 | openrouter | 262,144 | $0.633000 / $2.637500 | passthrough | — | streaming, tools | | openrouter/moonshotai/kimi-k2-thinking | openrouter | 262,144 | $0.633000 / $2.637500 | passthrough | — | streaming, tools | | openrouter/moonshotai/kimi-k2.5 | openrouter | 262,144 | $0.395625 / $2.136375 | passthrough | — | streaming, tools | | openrouter/moonshotai/kimi-k2.6 | openrouter | 262,144 | $0.6963 / $3.59755 | passthrough | — | streaming, tools | | openrouter/morph/morph-v3-fast | openrouter | 81,920 | $0.844000 / $1.266000 | passthrough | — | streaming, tools | | openrouter/morph/morph-v3-large | openrouter | 262,144 | $0.949500 / $2.004500 | passthrough | — | streaming, tools | | openrouter/nousresearch/hermes-3-llama-3.1-405b | openrouter | 131,072 | $1.055 / $1.055 | passthrough | — | streaming, tools | | openrouter/nousresearch/hermes-3-llama-3.1-70b | openrouter | 131,072 | $0.738500 / $0.738500 | passthrough | — | streaming, tools | | openrouter/nousresearch/hermes-4-405b | openrouter | 131,072 | $1.055 / $3.165 | passthrough | — | streaming, tools | | openrouter/nousresearch/hermes-4-70b | openrouter | 131,072 | $0.13715 / $0.422000 | passthrough | — | streaming, tools | | openrouter/nvidia/llama-3.3-nemotron-super-49b-v1.5 | openrouter | 131,072 | $0.422000 / $0.422000 | passthrough | — | streaming, tools | | openrouter/nvidia/nemotron-3-nano-30b-a3b | openrouter | 262,144 | $0.052750 / $0.211000 | passthrough | — | streaming, tools | | openrouter/nvidia/nemotron-3-super-120b-a12b | openrouter | 1,000,000 | $0.0844 / $0.474750 | passthrough | — | streaming, tools | | openrouter/openai/gpt-3.5-turbo | openrouter | 16,385 | $0.5275 / $1.5825 | passthrough | — | streaming, tools | | openrouter/openai/gpt-3.5-turbo-0613 | openrouter | 4,095 | $1.055 / $2.11 | passthrough | — | streaming, tools | | openrouter/openai/gpt-3.5-turbo-16k | openrouter | 16,385 | $3.165 / $4.22 | passthrough | — | streaming, tools | | openrouter/openai/gpt-3.5-turbo-instruct | openrouter | 4,095 | $1.5825 / $2.11 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4 | openrouter | 8,191 | $31.65 / $63.30 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4-turbo | openrouter | 128,000 | $10.550000 / $31.65 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4-turbo-preview | openrouter | 128,000 | $10.550000 / $31.65 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4.1 | openrouter | 1,047,576 | $2.11 / $8.44 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4.1-mini | openrouter | 1,047,576 | $0.422000 / $1.688000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4.1-nano | openrouter | 1,047,576 | $0.105500 / $0.422000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4o | openrouter | 128,000 | $2.637500 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4o-2024-05-13 | openrouter | 128,000 | $5.275000 / $15.825 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4o-2024-08-06 | openrouter | 128,000 | $2.637500 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4o-2024-11-20 | openrouter | 128,000 | $2.637500 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4o-mini | openrouter | 128,000 | $0.158250 / $0.633000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4o-mini-2024-07-18 | openrouter | 128,000 | $0.158250 / $0.633000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4o-mini-search-preview | openrouter | 128,000 | $0.158250 / $0.633000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-4o-search-preview | openrouter | 128,000 | $2.637500 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5 | openrouter | 400,000 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5-chat | openrouter | 128,000 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5-codex | openrouter | 400,000 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5-image | openrouter | 400,000 | $10.550000 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5-image-mini | openrouter | 400,000 | $2.637500 / $2.11 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5-mini | openrouter | 400,000 | $0.26375 / $2.11 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5-nano | openrouter | 400,000 | $0.052750 / $0.422000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5-pro | openrouter | 400,000 | $15.825 / $126.60 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.1 | openrouter | 400,000 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.1-chat | openrouter | 128,000 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.1-codex | openrouter | 400,000 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.1-codex-max | openrouter | 400,000 | $1.318750 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.1-codex-mini | openrouter | 400,000 | $0.26375 / $2.11 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.2 | openrouter | 400,000 | $1.84625 / $14.77 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.2-chat | openrouter | 128,000 | $1.84625 / $14.77 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.2-codex | openrouter | 400,000 | $1.84625 / $14.77 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.2-pro | openrouter | 400,000 | $22.155000 / $177.240000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.3-chat | openrouter | 128,000 | $1.84625 / $14.77 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.3-codex | openrouter | 400,000 | $1.84625 / $14.77 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.4 | openrouter | 1,050,000 | $2.637500 / $15.825 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.4-image-2 | openrouter | 272,000 | $8.44 / $15.825 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.4-mini | openrouter | 400,000 | $0.79125 / $4.7475 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.4-nano | openrouter | 400,000 | $0.211000 / $1.318750 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.4-pro | openrouter | 1,050,000 | $31.65 / $189.900000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.5 | openrouter | 1,050,000 | $5.275000 / $31.65 | passthrough | — | streaming, tools | | openrouter/openai/gpt-5.5-pro | openrouter | 1,050,000 | $31.65 / $189.900000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-audio | openrouter | 128,000 | $2.637500 / $10.550000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-audio-mini | openrouter | 128,000 | $0.633000 / $2.532000 | passthrough | — | streaming, tools | | openrouter/openai/gpt-chat-latest | openrouter | 400,000 | $5.275000 / $31.65 | passthrough | — | streaming, tools | | openrouter/openai/gpt-oss-120b | openrouter | 131,072 | $0.037980 / $0.189900 | passthrough | — | streaming, tools | | openrouter/openai/gpt-oss-20b | openrouter | 131,072 | $0.030595 / $0.1477 | passthrough | — | streaming, tools | | openrouter/openai/gpt-oss-safeguard-20b | openrouter | 131,072 | $0.079125 / $0.316500 | passthrough | — | streaming, tools | | openrouter/openai/o1 | openrouter | 200,000 | $15.825 / $63.30 | passthrough | — | streaming, tools | | openrouter/openai/o1-pro | openrouter | 200,000 | $158.25 / $633.00 | passthrough | — | streaming, tools | | openrouter/openai/o3 | openrouter | 200,000 | $2.11 / $8.44 | passthrough | — | streaming, tools | | openrouter/openai/o3-deep-research | openrouter | 200,000 | $10.550000 / $42.200000 | passthrough | — | streaming, tools | | openrouter/openai/o3-mini | openrouter | 200,000 | $1.1605 / $4.642 | passthrough | — | streaming, tools | | openrouter/openai/o3-mini-high | openrouter | 200,000 | $1.1605 / $4.642 | passthrough | — | streaming, tools | | openrouter/openai/o3-pro | openrouter | 200,000 | $21.100000 / $84.400000 | passthrough | — | streaming, tools | | openrouter/openai/o4-mini | openrouter | 200,000 | $1.1605 / $4.642 | passthrough | — | streaming, tools | | openrouter/openai/o4-mini-deep-research | openrouter | 200,000 | $2.11 / $8.44 | passthrough | — | streaming, tools | | openrouter/openai/o4-mini-high | openrouter | 200,000 | $1.1605 / $4.642 | passthrough | — | streaming, tools | | openrouter/perceptron/perceptron-mk1 | openrouter | 32,768 | $0.158250 / $1.5825 | passthrough | — | streaming, tools | | openrouter/perplexity/sonar | openrouter | 127,072 | $1.055 / $1.055 | passthrough | — | streaming, tools | | openrouter/perplexity/sonar-deep-research | openrouter | 128,000 | $2.11 / $8.44 | passthrough | — | streaming, tools | | openrouter/perplexity/sonar-pro | openrouter | 200,000 | $3.165 / $15.825 | passthrough | — | streaming, tools | | openrouter/perplexity/sonar-pro-search | openrouter | 200,000 | $3.165 / $15.825 | passthrough | — | streaming, tools | | openrouter/perplexity/sonar-reasoning-pro | openrouter | 128,000 | $2.11 / $8.44 | passthrough | — | streaming, tools | | openrouter/qwen/qwen-2.5-72b-instruct | openrouter | 131,072 | $0.379800 / $0.422000 | passthrough | — | streaming, tools | | openrouter/qwen/qwen-2.5-7b-instruct | openrouter | 131,072 | $0.0422 / $0.105500 | passthrough | — | streaming, tools | | openrouter/qwen/qwen-2.5-coder-32b-instruct | openrouter | 128,000 | $0.6963 / $1.055 | passthrough | — | streaming, tools | | openrouter/qwen/qwen-plus | openrouter | 1,000,000 | $0.2743 / $0.8229 | passthrough | — | streaming, tools | | openrouter/qwen/qwen-plus-2025-07-28 | openrouter | 1,000,000 | $0.2743 / $0.8229 | passthrough | — | streaming, tools | | openrouter/qwen/qwen2.5-vl-72b-instruct | openrouter | 131,072 | $0.844000 / $1.055 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-14b | openrouter | 131,702 | $0.105500 / $0.2532 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-235b-a22b | openrouter | 131,072 | $0.480025 / $1.920100 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-235b-a22b-2507 | openrouter | 262,144 | $0.094950 / $0.105500 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-235b-a22b-thinking-2507 | openrouter | 262,144 | $0.157723 / $1.577225 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-30b-a3b | openrouter | 131,072 | $0.1266 / $0.5275 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-30b-a3b-instruct-2507 | openrouter | 131,072 | $0.050798 / $0.203668 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-30b-a3b-thinking-2507 | openrouter | 131,072 | $0.13715 / $1.6458 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-32b | openrouter | 131,072 | $0.0844 / $0.2954 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-8b | openrouter | 131,072 | $0.123435 / $0.480025 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-coder | openrouter | 1,048,576 | $0.2321 / $1.899000 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-coder-30b-a3b-instruct | openrouter | 160,000 | $0.07385 / $0.28485 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-coder-flash | openrouter | 1,000,000 | $0.205725 / $1.028625 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-coder-next | openrouter | 262,144 | $0.11605 / $0.844000 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-coder-plus | openrouter | 1,000,000 | $0.68575 / $3.42875 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-max | openrouter | 262,144 | $0.8229 / $4.1145 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-max-thinking | openrouter | 262,144 | $0.8229 / $4.1145 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-next-80b-a3b-instruct | openrouter | 262,144 | $0.094950 / $1.1605 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-next-80b-a3b-thinking | openrouter | 262,144 | $0.102862 / $0.8229 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-vl-235b-a22b-instruct | openrouter | 262,144 | $0.211000 / $0.9284 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-vl-235b-a22b-thinking | openrouter | 131,072 | $0.2743 / $2.743 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-vl-30b-a3b-instruct | openrouter | 262,144 | $0.13715 / $0.5486 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-vl-30b-a3b-thinking | openrouter | 131,072 | $0.13715 / $1.6458 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-vl-32b-instruct | openrouter | 262,144 | $0.10972 / $0.43888 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-vl-8b-instruct | openrouter | 256,000 | $0.123435 / $0.480025 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3-vl-8b-thinking | openrouter | 256,000 | $0.123435 / $1.440075 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.5-122b-a10b | openrouter | 262,144 | $0.2743 / $2.1944 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.5-27b | openrouter | 262,144 | $0.205725 / $1.6458 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.5-35b-a3b | openrouter | 262,144 | $0.1477 / $1.055 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.5-397b-a17b | openrouter | 256,000 | $0.406175 / $2.584750 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.5-9b | openrouter | 262,144 | $0.105500 / $0.158250 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.5-flash-02-23 | openrouter | 1,000,000 | $0.068575 / $0.2743 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.5-plus-02-15 | openrouter | 1,000,000 | $0.2743 / $1.6458 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.5-plus-20260420 | openrouter | 1,000,000 | $0.316500 / $1.899000 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.6-27b | openrouter | 262,144 | $0.300675 / $2.532000 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.6-35b-a3b | openrouter | 262,144 | $0.1477 / $1.055 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.6-flash | openrouter | 1,000,000 | $0.197813 / $1.186875 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.6-max-preview | openrouter | 262,144 | $1.0972 / $6.5832 | passthrough | — | streaming, tools | | openrouter/qwen/qwen3.6-plus | openrouter | 1,000,000 | $0.342875 / $2.05725 | passthrough | — | streaming, tools | | openrouter/rekaai/reka-edge | openrouter | 16,384 | $0.105500 / $0.105500 | passthrough | — | streaming, tools | | openrouter/rekaai/reka-flash-3 | openrouter | 65,536 | $0.105500 / $0.211000 | passthrough | — | streaming, tools | | openrouter/relace/relace-apply-3 | openrouter | 256,000 | $0.896750 / $1.318750 | passthrough | — | streaming, tools | | openrouter/relace/relace-search | openrouter | 256,000 | $1.055 / $3.165 | passthrough | — | streaming, tools | | openrouter/sao10k/l3-lunaris-8b | openrouter | 8,192 | $0.0422 / $0.052750 | passthrough | — | streaming, tools | | openrouter/sao10k/l3.1-70b-hanami-x1 | openrouter | 16,000 | $3.165 / $3.165 | passthrough | — | streaming, tools | | openrouter/sao10k/l3.1-euryale-70b | openrouter | 131,072 | $0.896750 / $0.896750 | passthrough | — | streaming, tools | | openrouter/sao10k/l3.3-euryale-70b | openrouter | 131,072 | $0.68575 / $0.79125 | passthrough | — | streaming, tools | | openrouter/stepfun/step-3.5-flash | openrouter | 262,144 | $0.105500 / $0.316500 | passthrough | — | streaming, tools | | openrouter/tencent/hunyuan-a13b-instruct | openrouter | 131,072 | $0.1477 / $0.60135 | passthrough | — | streaming, tools | | openrouter/tencent/hy3-preview | openrouter | 262,144 | $0.066465 / $0.221550 | passthrough | — | streaming, tools | | openrouter/thedrummer/cydonia-24b-v4.1 | openrouter | 131,072 | $0.316500 / $0.5275 | passthrough | — | streaming, tools | | openrouter/thedrummer/rocinante-12b | openrouter | 65,536 | $0.26375 / $0.5275 | passthrough | — | streaming, tools | | openrouter/thedrummer/skyfall-36b-v2 | openrouter | 32,768 | $0.58025 / $0.844000 | passthrough | — | streaming, tools | | openrouter/thedrummer/unslopnemo-12b | openrouter | 32,768 | $0.422000 / $0.422000 | passthrough | — | streaming, tools | | openrouter/undi95/remm-slerp-l2-13b | openrouter | 6,144 | $0.474750 / $0.68575 | passthrough | — | streaming, tools | | openrouter/upstage/solar-pro-3 | openrouter | 128,000 | $0.158250 / $0.633000 | passthrough | — | streaming, tools | | openrouter/writer/palmyra-x5 | openrouter | 1,040,000 | $0.633000 / $6.33 | passthrough | — | streaming, tools | | openrouter/x-ai/grok-4.20 | openrouter | 2,000,000 | $1.318750 / $2.637500 | passthrough | — | streaming, tools | | openrouter/x-ai/grok-4.20-multi-agent | openrouter | 2,000,000 | $1.318750 / $2.637500 | passthrough | — | streaming, tools | | openrouter/x-ai/grok-4.3 | openrouter | 1,000,000 | $1.318750 / $2.637500 | passthrough | — | streaming, tools | | openrouter/xiaomi/mimo-v2.5 | openrouter | 1,048,576 | $0.110775 / $0.2954 | passthrough | — | streaming, tools | | openrouter/xiaomi/mimo-v2.5-pro | openrouter | 1,048,576 | $0.458925 / $0.917850 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-4.5 | openrouter | 131,072 | $0.633000 / $2.321 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-4.5-air | openrouter | 131,072 | $0.13715 / $0.896750 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-4.5v | openrouter | 65,536 | $0.633000 / $1.899000 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-4.6 | openrouter | 202,752 | $0.453650 / $1.835700 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-4.6v | openrouter | 131,072 | $0.316500 / $0.949500 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-4.7 | openrouter | 202,752 | $0.422000 / $1.84625 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-4.7-flash | openrouter | 202,752 | $0.0633 / $0.422000 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-5 | openrouter | 202,752 | $0.633000 / $2.0256 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-5-turbo | openrouter | 262,144 | $1.266000 / $4.22 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-5.1 | openrouter | 202,752 | $1.019130 / $3.202980 | passthrough | — | streaming, tools | | openrouter/z-ai/glm-5v-turbo | openrouter | 202,752 | $1.266000 / $4.22 | passthrough | — | streaming, tools | | openrouter/~anthropic/claude-haiku-latest | openrouter | 200,000 | $1.055 / $5.275000 | passthrough | — | streaming, tools | | openrouter/~anthropic/claude-opus-latest | openrouter | 1,000,000 | $5.275000 / $26.375 | passthrough | — | streaming, tools | | openrouter/~anthropic/claude-sonnet-latest | openrouter | 1,000,000 | $2.11 / $10.550000 | passthrough | — | streaming, tools | | openrouter/~google/gemini-flash-latest | openrouter | 1,048,576 | $1.5825 / $9.495 | passthrough | — | streaming, tools | | openrouter/~google/gemini-pro-latest | openrouter | 1,048,576 | $2.11 / $12.66 | passthrough | — | streaming, tools | | openrouter/~moonshotai/kimi-latest | openrouter | 262,144 | $0.6963 / $3.59755 | passthrough | — | streaming, tools | | openrouter/~openai/gpt-latest | openrouter | 1,050,000 | $5.275000 / $31.65 | passthrough | — | streaming, tools | | openrouter/~openai/gpt-mini-latest | openrouter | 400,000 | $0.79125 / $4.7475 | passthrough | — | streaming, tools | | perplexity/perplexity/glm-5.3 | perplexity | 1,048,576 | $1.40 / $4.40 | passthrough | — | streaming, tools | | perplexity/perplexity/glm-5.3-flash | perplexity | 1,048,576 | $0.15 / $0.50 | passthrough | — | streaming, tools | | perplexity/perplexity/kimi-k3 | perplexity | 1,048,576 | $3.00 / $15.00 | passthrough | — | streaming, tools | | perplexity/perplexity/nemotron-3-ultra-550b-a55b | perplexity | 262,144 | $0.25 / $2.50 | passthrough | — | streaming, tools | | perplexity/perplexity/sonar | perplexity | 128,000 | $0.25 / $2.50 | passthrough | — | streaming | | perplexity/pplx-embed-context-v1-0.6b | perplexity | 32,768 | $0.008 / $0.00 | passthrough | — | embedding | | perplexity/pplx-embed-context-v1-4b | perplexity | 32,768 | $0.05 / $0.00 | passthrough | — | embedding | | perplexity/pplx-embed-v1-0.6b | perplexity | 32,768 | $0.004 / $0.00 | passthrough | — | embedding | | perplexity/pplx-embed-v1-4b | perplexity | 32,768 | $0.03 / $0.00 | passthrough | — | embedding | | perplexity/sonar | perplexity | 128,000 | $1.00 / $1.00 | passthrough | — | streaming | | perplexity/sonar-deep-research | perplexity | 128,000 | $2.00 / $8.00 | passthrough | — | streaming, tools | | perplexity/sonar-pro | perplexity | 200,000 | $3.00 / $15.00 | passthrough | — | streaming | | perplexity/sonar-pro-search | perplexity | 200,000 | $3.00 / $15.00 | passthrough | — | streaming, tools | | perplexity/sonar-reasoning-pro | perplexity | 128,000 | $2.00 / $8.00 | passthrough | — | streaming, tools | | together/meta-llama-3.3-70b | together | 128,000 | $1.04 / $1.04 | passthrough | — | streaming, tools | | xai/grok-3-mini | xai | 128,000 | $0.30 / $0.50 | passthrough | — | streaming, tools | | xai/grok-4 | xai | 256,000 | $3.00 / $15.00 | passthrough | — | streaming, tools, vision | | xai/grok-4.1-fast | xai | 256,000 | $0.20 / $0.50 | passthrough | — | streaming, tools, vision | | xai/grok-4.20 | xai | 2,000,000 | $1.25 / $2.50 | passthrough | — | streaming, tools, vision | | xai/grok-4.20-0309-non-reasoning | xai | 1,000,000 | $1.25 / $2.50 | passthrough | — | streaming, tools, vision | | xai/grok-4.20-0309-reasoning | xai | 1,000,000 | $1.25 / $2.50 | passthrough | — | streaming, tools, vision | | xai/grok-4.20-multi-agent | xai | 2,000,000 | $1.25 / $2.50 | passthrough | — | streaming, tools, vision | | xai/grok-4.20-multi-agent-0309 | xai | 1,000,000 | $1.25 / $2.50 | passthrough | — | streaming, tools, vision | | xai/grok-4.3 | xai | 1,000,000 | $1.25 / $2.50 | passthrough | — | streaming, tools, vision | | xai/grok-4.5 | xai | 256,000 | $2.00 / $6.00 | passthrough | — | streaming, tools, vision | | xai/grok-4.6 | xai | 500,000 | $2.00 / $6.00 | passthrough | — | streaming, tools, vision | | xai/grok-4.7 | xai | 500,000 | $1.600000 / $4.80 | passthrough | — | streaming, tools | | xai/grok-build-0.1 | xai | 256,000 | $1.00 / $2.00 | passthrough | — | streaming, tools | # Models Billing & Metering Source: https://www.plungeai.com/docs/models/billing Planned: not available yet. Tracked as TI-74. ## What this will do Account status, price = cost × org markup, reserve-then-settle, spend caps. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Models Streaming Source: https://www.plungeai.com/docs/models/streaming Planned: not available yet. Tracked as TI-74. ## What this will do SSE chat completions, translated for non-OpenAI providers. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Workflows Quickstart Source: https://www.plungeai.com/docs/workflows/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do Run multi-agent CNL workflows inline or saved, sync or streamed. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Workflow Best Practices Source: https://www.plungeai.com/docs/workflows/best-practices Planned: not available yet. Tracked as TI-74. ## What this will do Write CNL that validates, runs, and fails loudly. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # The CNL workflow spec Source: https://www.plungeai.com/docs/workflows/core-concepts/cnl Planned: not available yet. Tracked as TI-74. ## What this will do Workflow structure, task types and validation rules. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Choosing agents and models Source: https://www.plungeai.com/docs/workflows/core-concepts/agents-and-models Planned: not available yet. Tracked as TI-74. ## What this will do Agent tiers, model pinning, and how much time parallel fan-out takes. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Workflow Runs Lifecycle Source: https://www.plungeai.com/docs/workflows/core-concepts/run-lifecycle Planned: not available yet. Tracked as TI-74. ## What this will do Create a run, read its status and result, cancel it. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Results and traces Source: https://www.plungeai.com/docs/workflows/core-concepts/results-and-traces Planned: not available yet. Tracked as TI-74. ## What this will do What a run returns and how to read its persisted trace. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Follow-ups and continuation Source: https://www.plungeai.com/docs/workflows/core-concepts/follow-ups Planned: not available yet. Tracked as TI-74. ## What this will do Chain a run with follow-ups and answer paused runs. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Interactive Chat Source: https://www.plungeai.com/docs/workflows/examples/chat Planned: not available yet. Tracked as TI-74. ## What this will do A multi-turn chat session over MCP. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Deep Research Source: https://www.plungeai.com/docs/workflows/examples/deep-research Planned: not available yet. Tracked as TI-74. ## What this will do A goal-driven research workflow that answers with sources. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Batch Enrichment Source: https://www.plungeai.com/docs/workflows/examples/enrichment Planned: not available yet. Tracked as TI-74. ## What this will do Enrich a list of items with a `type: batch` workflow, then run it to completion. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # Workflow Webhooks Source: https://www.plungeai.com/docs/workflows/webhooks Planned: not available yet. Tracked as TI-25. ## What this will do PlungeAI will call a URL you register when a workflow run finishes, so you do not have to hold a stream open or poll. Nothing sends outbound webhooks today. ## Use this today Follow a run live over SSE. Start a run without waiting and fetch the result later. # Parallel and batch tasks Source: https://www.plungeai.com/docs/workflows/parallel-tasks Planned: not available yet. Tracked as TI-74. ## What this will do Fan out with `type: parallel` and run a per-item pipeline with `type: batch`. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill reference that covers the same ground. # Generate a workflow from a goal Source: https://www.plungeai.com/docs/workflows/generate Planned: not available yet. Tracked as TI-74. ## What this will do `plungeai_build_workflow` turns a goal into CNL YAML. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Workflow streaming events Source: https://www.plungeai.com/docs/workflows/streaming-events Planned: not available yet. Tracked as TI-74. ## What this will do SSE engine frames from `execute-stream`. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # MCP servers inside workflows Source: https://www.plungeai.com/docs/workflows/mcp-tool-calling Planned: not available yet. Tracked as TI-74. ## What this will do Give a harness task MCP servers to call. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Saved workflows and versions Source: https://www.plungeai.com/docs/workflows/saved-workflows Planned: not available yet. Tracked as TI-74. ## What this will do Save, version and run workflows by id. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Agents Quickstart Source: https://www.plungeai.com/docs/agents/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do Execute a registry agent from a prompt: sync, async or streamed. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Conversations and personas Source: https://www.plungeai.com/docs/agents/features/conversations Planned: not available yet. Tracked as TI-74. ## What this will do Carry context with `messages[]` (partial: `prompt` stays required and threading depends on the agent), `persona`, and MCP `session_id` for reliable threading. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Output formats Source: https://www.plungeai.com/docs/agents/features/output-formats Planned: not available yet. Tracked as TI-74. ## What this will do JSON, YAML, Markdown or text via `format` and `Accept`. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Agent streaming Source: https://www.plungeai.com/docs/agents/features/streaming Planned: not available yet. Tracked as TI-74. ## What this will do OpenAI-shaped chunks with keepalives and `X-Execution-Id`. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Citations Source: https://www.plungeai.com/docs/agents/features/citations Planned: not available yet. Tracked as TI-76. ## What this will do Agent answers will carry source annotations that tie each claim to the page or document it came from. Agent results have no citation field today. ## Use this today A multi-step research workflow you can run today. # Web search tool agents Source: https://www.plungeai.com/docs/agents/features/web-search Planned: not available yet. Tracked as TI-74. ## What this will do Search the web through registry tool agents. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Outbound MCP runs Source: https://www.plungeai.com/docs/agents/features/mcp-tools Planned: not available yet. Tracked as TI-74. ## What this will do Connect catalog MCP servers to a run and call their tools. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Async execution and results Source: https://www.plungeai.com/docs/agents/features/async-results Planned: not available yet. Tracked as TI-74. ## What this will do `sync:false` returns 202; redeem the result later. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Direct requests Source: https://www.plungeai.com/docs/agents/examples/direct-requests Planned: not available yet. Tracked as TI-74. ## What this will do Ask a question, ask for JSON, send YAML and get Markdown. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Use an agent as a subagent tool Source: https://www.plungeai.com/docs/agents/examples/subagent Planned: not available yet. Tracked as TI-74. ## What this will do Wrap an agent call as a tool in your own orchestrator. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # OpenAI compatibility (agents plane) Source: https://www.plungeai.com/docs/agents/openai-compatibility Planned: not available yet. Tracked as TI-74. ## What this will do Which OpenAI request and stream fields the agents plane accepts. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Discovery Quickstart Source: https://www.plungeai.com/docs/discovery/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do Find agents, tools, models, skills and connectors before you call. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Recommend Source: https://www.plungeai.com/docs/discovery/recommend Planned: not available yet. Tracked as TI-74. ## What this will do Ask the registry which capability fits a task. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Search modes and fields Source: https://www.plungeai.com/docs/discovery/core-concepts/search-modes Planned: not available yet. Tracked as TI-74. ## What this will do `mode` hybrid, keyword or vector; `fields` list, summary or full. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # The card object Source: https://www.plungeai.com/docs/discovery/core-concepts/cards Planned: not available yet. Tracked as TI-74. ## What this will do What a registry card holds and how to read one. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Card status and kinds Source: https://www.plungeai.com/docs/discovery/core-concepts/card-status Planned: not available yet. Tracked as TI-74. ## What this will do Only `status:active` cards run; kinds you can filter on. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Inspect a tool contract Source: https://www.plungeai.com/docs/discovery/features/tool-contract Planned: not available yet. Tracked as TI-74. ## What this will do Read operations, required params and examples before executing. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Quality signals Source: https://www.plungeai.com/docs/discovery/features/quality Planned: not available yet. Tracked as TI-74. ## What this will do Add `include=quality` to search results. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Catalog resources over MCP Source: https://www.plungeai.com/docs/discovery/features/resources Planned: not available yet. Tracked as TI-74. ## What this will do Dropped: discovery search is synchronous and has no event stream. Replacement: read catalog lists as MCP resources. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Registry kinds Source: https://www.plungeai.com/docs/discovery/features/kinds Planned: not available yet. Tracked as TI-74. ## What this will do Dropped: there is no long-running discovery run to notify on. Replacement: the kinds you can search. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Pagination Source: https://www.plungeai.com/docs/discovery/features/pagination Planned: not available yet. Tracked as TI-74. ## What this will do Discovery search and `GET /v1/tools` page with `limit` and `offset` and return `count`; only `GET /v1/agents` returns `has_more` and `next_offset`. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Workflow templates Source: https://www.plungeai.com/docs/discovery/features/templates Planned: not available yet. Tracked as TI-74. ## What this will do Dropped: a search cannot be cancelled. Replacement: start from a template. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Discovery first Source: https://www.plungeai.com/docs/discovery/features/discovery-first Planned: not available yet. Tracked as TI-74. ## What this will do Dropped: there is no stateful FindAll-style run to refresh. Replacement: the discovery-first rule. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # Scheduling Quickstart Source: https://www.plungeai.com/docs/scheduling/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do Cron jobs for agents, queries, workflows and heartbeats. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Run now and run history Source: https://www.plungeai.com/docs/scheduling/runs Planned: not available yet. Tracked as TI-74. ## What this will do Trigger a job now and read its run history. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Webhook triggers Source: https://www.plungeai.com/docs/scheduling/webhook-triggers Planned: not available yet. Tracked as TI-25. ## What this will do You will be able to start a scheduled job or a workflow by calling a webhook URL on a public PlungeAI host. Inbound triggers are not available on a public host today. ## Use this today Run a workflow on a cron schedule with the plungeai_schedule MCP tool. # Heartbeat check Source: https://www.plungeai.com/docs/scheduling/examples/heartbeat Planned: not available yet. Tracked as TI-74. ## What this will do A heartbeat job that checks a condition and acts on it. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Trigger a workflow or mission Source: https://www.plungeai.com/docs/scheduling/examples/trigger-workflow Planned: not available yet. Tracked as TI-74. ## What this will do Run a workflow or mission on a schedule and notify a channel. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Scheduling: MCP tool → REST API Source: https://www.plungeai.com/docs/scheduling/migration-guide Planned: not available yet. Tracked as TI-24. ## What this will do When a REST schedules API exists, this guide will map each `plungeai_schedule` action to its REST route so existing jobs keep running. Scheduling is available only through the MCP tool today. ## Use this today Create and manage schedules with plungeai_schedule on `https://mcp.plungeai.com/v1`. # Tools Quickstart Source: https://www.plungeai.com/docs/tools/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do List tool agents, read a contract, execute one operation. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Outcomes and remediation Source: https://www.plungeai.com/docs/tools/outcomes Planned: not available yet. Tracked as TI-74. ## What this will do The seven outcomes, the remediation actions, and their HTTP statuses. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Tool catalog by category Source: https://www.plungeai.com/docs/tools/catalog Planned: not available yet. Tracked as TI-74. ## What this will do The tool agents available today, by category. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Long runs (async) Source: https://www.plungeai.com/docs/tools/async Planned: not available yet. Tracked as TI-74. ## What this will do `mode: "async"` for tool calls that take longer than one request. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Missions Quickstart Source: https://www.plungeai.com/docs/missions/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do Run a bounded agent loop with a goal, allowed tools and an iteration cap. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Mission bounds and the tool fence Source: https://www.plungeai.com/docs/missions/bounds Planned: not available yet. Tracked as TI-74. ## What this will do What a mission may do and where it stops. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Skills, plugins and capability injection Source: https://www.plungeai.com/docs/missions/capability-injection Planned: not available yet. Tracked as TI-74. ## What this will do Inject skills, experts, persona, backgrounds, plugins and MCP. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Pre-built agent cards Source: https://www.plungeai.com/docs/missions/prebuilt-agents Planned: not available yet. Tracked as TI-74. ## What this will do Run a catalog agent card with `mission_ref` or `pack`. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Async missions: the Ten-Minute Relay Source: https://www.plungeai.com/docs/missions/async-relay Planned: not available yet. Tracked as TI-74. ## What this will do Long missions run in resumable legs; poll for the result. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Connectors and connected accounts Source: https://www.plungeai.com/docs/connectors/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do OAuth and API-key connectors, and how a connected account reaches a call. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # The connector proxy Source: https://www.plungeai.com/docs/connectors/proxy Planned: not available yet. Tracked as TI-74. ## What this will do Call a provider's API raw through `api.plungeai.com/v1/connect` with your `ozk_` key (`$PLUNGE_API_KEY`). This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Traces Quickstart Source: https://www.plungeai.com/docs/traces/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do Read the spans and gateway log of any run by trace id. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Execution history and cost Source: https://www.plungeai.com/docs/traces/history-and-cost Planned: not available yet. Tracked as TI-74. ## What this will do List past runs and what they cost. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # The activity log Source: https://www.plungeai.com/docs/traces/activity-log Planned: not available yet. Tracked as TI-74. ## What this will do Your key's call log on the MCP server. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # One Agent Source: https://www.plungeai.com/docs/one-agent/overview One Agent (`one-agent`) is the platform's agent runtime. It is one agent id with two modes, and the task decides which one runs. Saved bots run on it too: a bot is a saved workflow whose single task is a One Agent loop (see [Bots](/bots/overview)). | Mode | Runs when | What you get | Page | |---|---|---|---| | Call mode | The task carries no loop command | One model answer: any provider and model, `persona`, `skill`, conversation history, streaming, `format` | [Call mode](/one-agent/call-mode) | | Loop mode | The task carries a loop command | A bounded agentic run: tools inside a fence, a turn cap, memory recall, sub-agents, virtual files, approval gates | [Loop mode](/one-agent/loop-mode) | ## The mode rule A task loops only when it carries a loop command: - a turn count: `maxTurns`, `max_turns` or `max_iterations` of 1 or more, or - a `type: harness` task, which the engine marks as a mission run. `tools` and `effort` are not loop commands. With no loop command the task is answered once, in call mode. A continue (`session_id` plus a new `prompt`, no loop command) resumes the session in the mode it started in. ## Reach it from each door | Door | Call | Mode | |---|---|---| | REST agents plane | `POST /v1/agents/one-agent/execute` | Call. The loop fields (`goal`, `mission`, `allowed_tools`) are ignored on this plane. | | REST workflows plane | `POST /v1/workflows/execute`, or a saved workflow, with a `type: harness` task and `agent: one-agent` | Loop | | MCP | `plungeai_execute_agent` with `agent: one-agent` | Call | | MCP | `plungeai_run_mission` (the loop runtime defaults to `one-agent`; async by default) | Loop | | CNL | `agent: one-agent` in a `type: task`, `type: harness`, `parallel`, `sequential`, `debate`, `validate` or batch task | Call, or loop with a turn count or `type: harness` | In a `type: harness` task the engine marks the run as a mission, so it always loops. The task's `allowed_tools` and `max_iterations` are the fence and the cap. ## Models and keys Every model call goes through the provider layer. Loop mode needs a provider that returns tool calls: anthropic, openai, gemini, groq, xai, openrouter, cerebras, cohere, deepinfra, vercel and the other OpenAI-compatible ones. A loop on any other provider is refused with `outcome: needs_input`. The platform key is the default. A customer who saved their own key for that provider uses it instead. ## Next steps Single answers: fields, personas, skills, conversation history. Bounded agentic runs: the loop command, the fence, sessions and stop reasons. Every tool a loop can call, and which ones need a saved bot. How a tool call becomes allow, ask or deny. # One Agent call mode Source: https://www.plungeai.com/docs/one-agent/call-mode Call mode is what `one-agent` does when a task says nothing about looping. It answers once with the contract of `llm-agent`: any provider and model, a persona or skill, conversation history, streaming, and a choice of response format. ## A first call ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"In one sentence: what is a content delivery network?","sync":true,"max_tokens":60}' ``` The answer is in `content`. The request fields are listed on [Execute an agent](/api-reference/agents/execute-agent). ## Fields | Field | Type | Meaning | |---|---|---| | `prompt` (alias `input`) | string | The question or instruction. Required unless `messages` has a user turn. | | `messages` | array | Prior chat turns `[{role, content}]`; the model answers from the whole conversation. | | `system` | string | System prompt. | | `provider`, `model` | string | Pin a provider and model. Send them together; the model is checked against the catalog. The default provider is `gemini`. | | `persona` | string | A persona id. It is the only capability field the agents plane forwards. | | `max_tokens` | number | Output cap. | | `temperature`, `top_p`, `reasoning_effort`, `thinking_level` | | Sampling and reasoning hints forwarded to the model. | | `stream` | boolean | `true` returns an OpenAI-shaped stream that ends with `data: [DONE]`. | | `sync` | boolean | Default `true`. `false` returns a `202` pointer; redeem it with `GET /v1/agents/results/{workflowId}/{taskId}`. | | `format` | string | `json`, `yaml`, `markdown` or `text` (see [Four wires](/api-reference/four-wires)). | | `user_context` | object | The caller's time zone, locale and place. Default: derived from the request's network location. | A persona, skill or expert id is resolved by the same capability layer that serves bots and missions, so one id means the same thing everywhere. ## Conversation - `messages` carries a conversation inside one call. - In a workflow task, `memory: true` with a `memory_scope` loads the stored conversation history for that scope. - A task with `session_id` and a new `prompt` continues a stored session. Over MCP, `plungeai_execute_agent` takes `session_id` and `plungeai_continue` answers a pause. - A call with nothing to answer is refused with `needs_input` rather than sent to the model. ## In a workflow task A `type: task` step with `agent: one-agent` takes the same fields, and also `skill` or `skills`, `expert` or `experts`, and `memory: true`. Send it to `POST /v1/workflows/execute` with `Content-Type: text/yaml`: ```yaml workflow: name: skill-demo tasks: - id: assess type: task agent: one-agent prompt: "Assess a company with revenue 120M (+18% YoY), gross margin 72%, operating margin -4%, cash 80M, burn 15M per year. Three sentences." skill: financial-analysis maxTokens: 200 ``` The answer comes back as `content` of the final task, in the wire you sent (YAML in, YAML out; see [Four wires](/api-reference/four-wires)). ## Answer as a persona ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"Index fund or individual stocks for a 30-year-old? Two sentences.","persona":"warren-buffett","max_tokens":120}' ``` ## Need tools or more than one turn? Add a loop command and the same id runs [loop mode](/one-agent/loop-mode). # One Agent loop mode Source: https://www.plungeai.com/docs/one-agent/loop-mode Loop mode is a bounded agentic run: the model calls tools inside a fence, reads the results and goes again until it calls `task_complete` or a cap stops it. It is what a `type: harness` task with `agent: one-agent` runs, and it is what every [bot](/bots/overview) runs. ## Start a loop A task loops when it carries a loop command: `max_turns` or `max_iterations` of 1 or more, or `type: harness`. Over REST that is a workflow on the workflows plane. This run is capped at 2 iterations and injects one skill: ```bash curl -s https://api.plungeai.com/v1/workflows/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"workflow":{"name":"cap-demo","tasks":[{"type":"harness","id":"h1","agent":"one-agent","goal":"Reply with the single word pong, then call task_complete.","mission":"You are a terse assistant. Answer in one word.","allowed_tools":["task_complete"],"max_iterations":2,"skills":["report"]}]}}' ``` Over MCP, `plungeai_run_mission` takes a `goal`, an optional `mission`, `allowed_tools`, `max_iterations` (1 to 50), `success_criteria`, `persona` and `skills`; it runs on `one-agent` and answers async by default. Unlike a harness task, it has its own defaults: `allowed_tools` is `web_search`, `web_fetch` and `task_complete`, and `max_iterations` is 8. ## The task keys Write them flat on the harness task. The engine folds them into the mission and forwards the bot keys as written. | Key | Meaning | |---|---| | `goal` or `prompt` | What the run is for. | | `mission` | The run's purpose text. A whitespace-free string is read as the id of a pre-built agent card instead. A custom `mission` replaces the harness's own system prompt, so keep an instruction to call `task_complete`. | | `allowed_tools` | The fence. Fail-closed: a tool not listed never runs. Without it, a task on `agent: one-agent` has [every tool in the runtime](/one-agent/tools#without-allowed_tools). | | `max_iterations` (alias `max_turns`) | The turn cap. `effort` (`quick`, `standard`, `deep`) sets a default cap when no number is given. | | `success_criteria` | Added to the system prompt and self-checked by the agent; there is no separate verifier. | | `skills`, `experts`, `persona`, `backgrounds`, `plugins`, `mcp` | Capabilities injected into the run. | | `model`, `provider`, `effort`, `max_tokens` | Model choice. `effort` reaches the provider as `reasoning_effort` only when the caller set it. | | `allowed_agents`, `denied_agents` | Which platform agents `call_agent` may reach. | | `permissions`, `permission_locks` | Allow, ask or deny per tool or class. See [Policy and review](/one-agent/policy-and-review). | | `budget_usd_run` | Per-run spend cap in USD. | | `peers`, `posture`, `review`, `max_runtime_s`, `workspace` | The always-on bot keys. See the [playbook reference](/bots/playbook-reference). | ## Sessions and pauses - A run that needs a person pauses. `ask_user` pauses on a question and a gated tool call pauses on an approval. Answer with `POST /v1/executions/{id}/continue` or MCP `plungeai_continue`. - A running loop can be steered: `POST /v1/executions/{id}/steer` queues a message the loop reads at its next turn. A finished run answers `409`. - A continue (`session_id` plus a new `prompt`) resumes the session in loop mode, with its stored state. - Long runs continue in legs of about ten minutes. What must survive a leg or a continue (the denial streak, handed-off calls, the run's taint) rides the session. ## How a run stops | Stop | Cause | |---|---| | `task_complete` | The model called it with the result. | | Turn cap | `max_iterations` reached. | | `stop_reason: budget` | The running token cost passed `budget_usd_run`. | | `stop_reason: timeout` | `max_runtime_s` elapsed, checked between turns. The run does not yield or resume. | | `stop_reason: denied` | Three denied tool calls in a row on an unattended run. An interactive run pauses on a question instead. | | `stop_reason: review` | The reviewer denied three calls in a row. | | Cancel | `POST /v1/executions/{id}/cancel`. It also cancels the child runs the loop started. | A loop on a provider that cannot return tool calls is refused with `outcome: needs_input` before it starts. ## Sub-agents and files - `delegate` runs focused sub-agents in parallel, each with only the tools you grant it. A child with no `agent` or `mission_ref` runs on `one-agent`. - The file tools (`read_file`, `write_file`, `edit_file`, `list_files`, `search_files`, `delete_file`) work on a virtual workspace. `workspace: run` starts it clean each run; `workspace: bot` keeps it across runs. See [Workspace and inbox](/bots/workspace-and-inbox). - `recall` and `recall_history` search your own past runs; `memory` keeps durable facts. See [Tools](/one-agent/tools). # One Agent tools Source: https://www.plungeai.com/docs/one-agent/tools A loop can only call the tools its fence lists. The fence is `allowed_tools` on the harness task; a tool not listed never runs. This page lists what exists. How a call is allowed, asked or denied is on [Policy and review](/one-agent/policy-and-review). ## Without `allowed_tools` On `agent: one-agent`, a task that names no `allowed_tools` gets every tool on this page. Name `allowed_tools` to narrow it. `universal-agent`, the concierge runtime, has a smaller default fence: the core eight below. ## The core eight | Tool | Does | |---|---| | `registry_search` (alias `find_agent`) | Searches the platform registry for agents, workflows, connectors and capability cards. | | `registry_lookup` (alias `agent_card`) | Reads one card in full: parameters, prompt format, examples. | | `call_agent` (alias `run_agent`) | Runs a platform agent by id and operation. | | `web_search` | Web search. | | `web_fetch` | Fetches a public URL. It follows up to 5 redirects by hand, checks every hop is public, and refuses https to http. | | `load_skill` | Loads a skill, persona, expert or background that was not already injected. | | `ask_user` | Pauses the run on a question for the person. | | `task_complete` | Ends the run with the result. | A fence that lists `allowed_tools` must include `task_complete`, or the run cannot finish cleanly. ## The other tools | Tool | Does | |---|---| | `read_file`, `write_file`, `edit_file`, `list_files`, `search_files`, `delete_file` | A virtual workspace. `workspace: bot` keeps it across runs. | | `memory` | Durable facts that survive across sessions. One call applies a batch of `operations` (`add`, `replace`, `remove`) to the `user` or `memory` target. `read` and `forget {query}` are single calls of their own; `forget` purges every entry containing the text. | | `knowledge` | The user's knowledge base: `query`, `get_page`, `list_pages`, `save`, `add_source`, `ingest`, `lint`. | | `recall`, `recall_history` | Search your own past runs and reuse a prior result instead of redoing the work. | | `skill_manage` | Create, patch or delete a private skill. Never on by default; every write pauses for approval unless `permissions: { skill_manage: allow }`. | | `delegate` | Runs focused sub-agents in parallel, each with only the tools you grant it. | | `invoke_workflow` | Runs a CNL workflow, from a template id or inline YAML. | | `run_python` | Runs Python for calculations and data transforms. | | `local_agent` | Runs an action on a local agent on the owner's machine, over the tunnel. | | `platform_action` | Asks the user's client to perform a platform action; the run pauses until the client resumes it. | | `mcp_tool_search` | Loads MCP tool schemas on demand (see below). | ### MCP tools MCP tools reach the loop under names that start with `mcp__`. Past 15 tools or 8 KB of schemas, the system prompt lists them by name only and `mcp_tool_search` loads up to 5 schemas for the next turn. A continue or a new leg forgets them: search again. The search never offers a tool the run's permissions or posture refuse. ## Bot tools When you narrow the fence, list the ones you want in `allowed_tools`. `schedule`, `message_bot` and the `browser` bot session need a saved bot, because the engine stamps the bot id and owner on the run. `code` needs no saved bot, only a connected OceanCode. | Tool | Input | Does | |---|---|---| | `schedule` | `action`: `create`, `list`, `pause`, `resume`, `delete`, `run_now`, `wake` | Manages the bot's own schedule. See [Schedules and wake](/bots/schedules-and-wake). | | `message_bot` | `to`, `text`, `wait?` | Messages another bot, an A2A agent or a bot group. See [Bot to bot and groups](/bots/bot-to-bot-and-groups). | | `browser` | `steps`, `url?`, `session?` (`bot` or `none`) | Drives a cloud browser. `session: bot` (the default for a saved bot) keeps the bot's cookies between runs. Screenshots are saved under `/shots/` in the bot's files. | | `code` | `prompt`, `directory?`, `session_id?`, `timeout_s?` | Hands one coding turn to the owner's OceanCode on their machine, over the Local Tunnel. `timeout_s` defaults to and is capped at 300. Pass the returned `session_id` to continue. With no OceanCode connected the tool says so. | ## Permission classes Every call has a class. The class is what `permissions`, `posture` and the stored presets match on. | Tool or case | Class | |---|---| | A guarded money category | `pay` | | `message_bot` | `send` | | `code` | `write` | | `schedule` | by action: `delete` is `delete`, `list` is `read`, the rest are `write` | | `browser` | `write` when a step clicks, types, runs script or presses keys, otherwise `read` | | `mcp__*` | from the server's own hints: destructive is `delete`, read-only is `read`, anything else `write` | | `local_agent` | `write` for actions that change the machine, otherwise `read` | # One Agent policy and review Source: https://www.plungeai.com/docs/one-agent/policy-and-review Every tool call in a loop is classified, then decided. One function makes the decision, so every door (Studio, REST, MCP, the CLI, the scheduler) gets the same answer, and the rung that decided is written to the run's audit row. ## Classes and presets A call has one of five classes: `read`, `write`, `send`, `delete` or `pay` (how each tool gets its class is on [Tools](/one-agent/tools#permission-classes)). The `permissions` map on a task, and the stored presets on a saved bot, give a class or a tool name one preset: | Preset | Effect | |---|---| | `allow` | The call runs. | | `ask` | The run pauses for approval. | | `deny` | The call does not run; the model gets an error that names the reason. | | `hand_off` | A `deny` that does not retry. The call never runs, and the run's result carries a `[handed off to you — not run: …]` line so the owner knows it is theirs to do. A hand-off does not count toward the denial breaker. | A preset this build does not recognise is treated as `ask`, never `allow`. `hand_off` is understood only by One Agent; for any other agent the engine rewrites it to `deny`. ## The decision order The first rung that applies decides: 1. **Posture `read_only`:** `write`, `send`, `delete` and `pay` are denied. So are `delegate`, `invoke_workflow` and every MCP tool, and any tool with an unknown class (fail closed). 2. **Explicit `deny`:** on the tool's name or on its class. Posture adds a class `deny` on every mutating class. 3. **`hand_off`.** 4. **The money floor:** an operation in the always-gated set (`buy`, `purchase`, `pay`, `transfer`, `send`, `send_payment`, `withdraw`, `fetch_paid`, `shop`) is always `ask`. No `allow` lowers it, and it pauses the run even when unattended. 5. **The preset:** the tool's own entry, then its class entry. Anything other than `allow` asks. 6. **An admin lock:** a locked class lifts `allow` to `ask`. It never lifts a `deny`. 7. **Otherwise `allow`.** The presets on the task and the presets stored on the saved bot are merged before the run starts: the task's own entries win per class, the bot's locks replace what the YAML carried, and the lower per-run budget wins. See [Permissions and budget](/bots/permissions-and-budget). ## Posture `posture: read_only` makes a run safe to point at the open web. A bot's own `permissions` cannot lift it, and an admin lock never turns a deny into an allow. The `browser` tool still reads pages under it. Any value other than `read_only` is forced to `read_only`: a typo never widens a run. ## Review `review: auto` adds one model call that reviews each `write`, `send`, `pay` or `delete` call the permissions allowed. It answers allow, ask the owner (the normal approval pause) or deny (a tool error that names the reason). A reviewer error asks and never allows. Three denials in a row stop the run with `stop_reason: review`. It costs one model call per gated action. Any other value is forced to `auto`. ## The denial breaker Three denied calls in a row, with no executed call between them, stop an unattended run with `stop_reason: denied`. An interactive run pauses and asks the person how to proceed. Any executed call resets the count. ## Secrets stay out of results Tool results, spilled files, the outcome of an approval and the saved result pass through a redactor first. JWTs, bearer tokens and `sk-` or `ozk_` keys become `[REDACTED:]`. Card numbers, IBANs and email addresses pass, because they are the owner's own data. ## Untrusted content Text someone else wrote is data, not instructions. The results of `web_search`, `web_fetch`, `browser`, `message_bot`, every `mcp__*` tool and `call_agent` calls to search, MCP or scraping agents are scanned for instruction patterns. A result that matches is wrapped in an `` marker listing the patterns it carries; a clean result is returned as it is. Either way the run is marked tainted. Today the taint is recorded in the audit trail only: a later `write` or `send` call writes a row saying it would have been asked, and nothing is blocked because of it. `code` results are not treated as untrusted. `web_fetch` refuses private and link-local addresses. ## Other run limits | Limit | Key | Stops with | |---|---|---| | Spend per run | `budget_usd_run` | `stop_reason: budget` | | Wall-clock time | `max_runtime_s` | `stop_reason: timeout` | | Turns | `max_iterations` | the turn cap | Cancelling a run also cancels the child runs it started with `delegate` or `message_bot wait: true`, and the OceanCode sessions it started with `code`. # Bots Source: https://www.plungeai.com/docs/bots/overview A bot is a saved workflow with `kind: bot`. Its one task is a [One Agent loop](/one-agent/loop-mode) with a mission. It is not a separate runtime: what makes it a bot is the wiring around the YAML, which is where the schedule, the chats, the triggers, the policy and the budget live. | A bot has | Set by | Page | |---|---|---| | A mission and a tool fence | The YAML | [Playbook reference](/bots/playbook-reference) | | A clock | `schedule` tool, a schedule on the bot | [Schedules and wake](/bots/schedules-and-wake) | | Teammates | `peers`, bot groups, A2A | [Bot to bot and groups](/bots/bot-to-bot-and-groups) | | Chats and a web widget | Pairing and bindings | [Channels and voice](/bots/channels-and-voice) | | Rules and a spend cap | Stored presets, locks, budgets | [Permissions and budget](/bots/permissions-and-budget) | | Files, an inbox and memory | `workspace`, `memory` | [Workspace and inbox](/bots/workspace-and-inbox) | | Outside events | Webhook, email, Slack, Gmail, Git | [Triggers and hooks](/bots/triggers-and-hooks) | | A control surface on every door | REST, MCP, CLI | [REST, MCP and CLI](/bots/rest-mcp-cli) | ## A minimal bot ```yaml workflow: name: Competitor Watch description: Checks a page for pricing changes tasks: - type: harness id: watch agent: one-agent goal: "Check {input} for pricing changes" max_iterations: 8 allowed_tools: [web_search, web_fetch, task_complete] mission: | You are Competitor Watch. Reply NO-CHANGE when nothing changed. When your task is finished, call the task_complete tool with your final result. ``` Save it as a bot over REST: ```bash 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 allowed_tools: [web_search, web_fetch, task_complete]\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"}' ``` In Studio, choose **New**, then **Bot agent**; the bot opens in the same YAML editor as a flow. The bot templates (`GET /v1/templates?kind=bot`) are ready-made starting points; `POST /v1/templates/{id}/use` copies one into your account. ## What a save checks Saving runs the CNL schema and then the kind lint, before anything is written. A refusal is `422 invalid_workflow` with `errors` and `warnings`, and the stored row is unchanged. - A bot is exactly one task, and it is `type: harness`. - A declared `allowed_tools` fence must include `task_complete`. A mission that never mentions `task_complete` is a warning. - `memory_owner` is refused: a bot's memory tenant comes from the user who runs it. - A local bot (`local: true` or `local_agents`) should set `agent: one-agent`. `harness-agent` and `agentic-agent` are still accepted until the switch-over; `universal-agent` never is. - `posture`, `peers`, `review`, `max_runtime_s` and `workspace` apply only on `agent: one-agent`. On `harness-agent`, `agentic-agent` or `universal-agent` they are an error, because those runtimes would ignore them. - A hard-coded email address or UUID in the YAML is a warning. ## Identity and who may use what When a saved bot runs, the engine tells the loop which bot it is (`bot_id`) and who owns it (`bot_owner`). The inbox, the `schedule` and `message_bot` tools, the `browser` bot session and the persistent workspace need that identity. - These abilities belong to the owner. A teammate who runs a shared bot runs it without an identity. - A saved workflow that is not a bot but uses `agent: one-agent`, run by its owner, is treated like a bot. - Deleting a bot deletes its scheduled jobs and wipes its own memory namespace (the one a web widget uses). What the bot saved with `memory` while you ran it lives in your own memory and stays; remove it with `POST /v1/memory/forget`. ## Every run uses the stored policy The engine loads the bot's stored policy once per run, on every door: a run by id, a schedule, a trigger, a chat message, MCP and Studio all stop at the same caps and holds. See [Permissions and budget](/bots/permissions-and-budget). # Bot playbook reference Source: https://www.plungeai.com/docs/bots/playbook-reference Every key a One Agent bot reads, one row each. This page is generated from the playbook reference template, `scripts/bot-templates/bot-playbook-reference.yaml`, so the table and the template cannot disagree. Copy the lines you need into the harness task of a bot and delete the rest; the order of keys does not matter. The keys `posture`, `peers`, `review`, `max_runtime_s` and `workspace` apply only on `agent: one-agent`; saving them on another loop runtime is refused. See [Bots](/bots/overview#what-a-save-checks). ## Doors | Door | What it is | |---|---| | YAML | Any door that saves the bot's YAML: Studio's Code tab, `POST /v1/workflows` and `PATCH /v1/workflows/{id}`, MCP `plungeai_workflow` `create` and `update`, `ocean-cli workflows create` and `update`. | | Studio | The `bot-config` block of Studio's Think tab, which writes these keys into the YAML for you; the bot's Settings panel edits some of them too. | | Stored policy | The permission presets and the spend cap stored on the saved bot: `PUT /v1/workflows/{id}/permissions`, MCP `permissions_set`, `ocean-cli workflows permissions set`, and Studio's Permissions and Budget fields. They are merged with the YAML on every run ([Permissions and budget](/bots/permissions-and-budget)). | ## Keys | Key | Type | Default | What it does | Set by | |---|---|---|---|---| | `type` | string | none, required | A bot is exactly one task of type harness (a mission-bounded loop run). | YAML | | `id` | string | none, required | The task id; the run's result is stored under it. | YAML | | `agent` | string | harness-agent on a task with no agent line | The runtime that runs the loop; only one-agent reads the bot keys below. | YAML | | `goal` | string | none, required | What this run is asked to do; `{input}` is replaced by the run's input. | YAML | | `mission` | string | none, required | The standing instructions (system prompt); end it by telling the bot to call `task_complete`. | YAML | | `workspace` | string | bot for a saved bot | `bot` keeps the bot's virtual files across runs; run starts every run clean. | YAML, Studio | | `posture` | string | unset (not read-only) | `read_only` denies every write, send, delete and pay (and delegate, `invoke_workflow`, MCP tools) whatever permissions says. | YAML, Studio | | `peers` | list | none (no 1:1 messaging) | The only bots, or `a2a:` agents, that `message_bot` may reach by name or id; a `group:` send needs no peers. | YAML, Studio | | `review` | string | unset (no review) | `auto` has a model check each allowed write, send, delete or pay call before it runs (one extra call each). | YAML, Studio | | `max_runtime_s` | number | unset (no cap beyond the turn cap) | Wall-clock cap in seconds for the whole run; the run stops with `stop_reason` timeout. | YAML, Studio | | `budget_usd_run` | number | unset (no cap); a lower cap stored on the saved bot wins | Per-run spend cap in USD, above 0 (blank = no cap; 0 is refused by the YAML block and Settings, and the runtime ignores a non-positive stored value); the loop stops at the next turn once cost passes it. | YAML, Stored policy | | `permissions` | map | allow, except the money floor which always asks | Per-class or per-tool presets for send, write, pay and delete; reads are allowed unless you name `read` or the tool. | YAML, Stored policy | | `permissions.send` | string | allow, but a registry-agent call with operation send is always gated (ALWAYS_GATED) even with send: allow | Preset for outward messages (email, chat, `message_bot`); ask pauses the run for the owner's approval. | YAML, Stored policy | | `permissions.write` | string | allow | Preset for calls that change something (files, schedule create, browser clicks). | YAML, Stored policy | | `permissions.pay` | string | allow, but payment ops always ask | `hand_off` never runs the call and tells the owner it is theirs to do; the result ends with a `[handed off to you]` line. | YAML, Stored policy | | `permissions.delete` | string | allow | Preset for destructive calls; deny refuses them. | YAML, Stored policy | | `allowed_tools` | list | every tool of the runtime on agent one-agent (universal-agent's default is the 8-tool DEFAULT_FENCE); list tools to narrow it | The tool fence; only these tools reach the model and `task_complete` must be in it. | YAML, Studio | | `allowed_tools.schedule` | tool name | available when `allowed_tools` is absent; name it to keep it in a narrowed fence | The bot schedules itself (create, list, pause, resume, delete, `run_now`, wake); needs a saved bot. | YAML, Studio | | `allowed_tools.message_bot` | tool name | available when `allowed_tools` is absent; name it to keep it in a narrowed fence | Sends to a peer or `group:` (async by default, replies arrive as `[MESSAGE from ...]` in a later run); needs a saved bot. | YAML, Studio | | `allowed_tools.browser` | tool name | available when `allowed_tools` is absent; name it to keep it in a narrowed fence | Drives a cloud browser in one session, with the bot's saved login (click and type count as writes). | YAML, Studio | | `allowed_tools.memory` | tool name | available when `allowed_tools` is absent; name it to keep it in a narrowed fence | Add, replace, remove, read, or forget `{query}` to purge matching entries. | YAML, Studio | | `allowed_tools.code` | tool name | available when `allowed_tools` is absent; name it to keep it in a narrowed fence | Hands one turn (max 300 s) to the owner's OceanCode over the Local Tunnel; no daemon connected means no tool. | YAML, Studio | | `allowed_agents` | list | all except payment agents | The registry agents `call_agent` may reach (payment agents only when named). | YAML | | `skills` | list | none | Registry skill ids injected into the system prompt, at most 5. | YAML, Studio | | `persona` | string | none | One persona id that sets the voice. | YAML, Studio | | `max_turns` | number | the effort preset's cap, else the runtime's own | Turn cap for the loop (`max_iterations` is the legacy spelling). | YAML, Studio | | `effort` | string | unset | `quick`, standard or deep sets the default turn cap and how wide the bot works. | YAML | ## The whole file ```yaml # Playbook reference: every key a one-agent bot reads, set once, with one comment line above it. # Each comment reads "what it does. Default: what you get when the key is absent." # Copy the lines you need and delete the rest. Order in a real bot does not matter. workflow: name: "Playbook Reference" description: "Every one-agent bot key, set and explained in comments. Copy the lines you need." tasks: # type: a bot is exactly one task of type harness (a mission-bounded loop run). Default: none, required. - type: harness # id: the task id; the run's result is stored under it. Default: none, required. id: reference # agent: the runtime that runs the loop; only one-agent reads the bot keys below. Default: harness-agent on a task with no agent line. agent: one-agent # goal: what this run is asked to do; {input} is replaced by the run's input. Default: none, required. goal: "{input}" # mission: the standing instructions (system prompt); end it by telling the bot to call task_complete. Default: none, required. mission: | You are Playbook Reference, a bot that exists to show every bot key in its own YAML. What each key does is written as a comment in that YAML, which you cannot see, so never explain a key from memory. Reply with REFERENCE-OK, a one-line echo of the goal, and the sentence "Read the comment above each key in this playbook." Your persona only sets the voice; this reply format always wins. When your task is finished, call the task_complete tool with your reply. # workspace: bot keeps the bot's virtual files across runs; run starts every run clean. Default: bot for a saved bot. workspace: bot # posture: read_only denies every write, send, delete and pay (and delegate, invoke_workflow, MCP tools) whatever permissions says. Default: unset (not read-only). posture: read_only # peers: the only bots, or "a2a:" agents, that message_bot may reach by name or id; a group: send needs no peers. Default: none (no 1:1 messaging). peers: ["Playbook Peer"] # review: auto has a model check each allowed write, send, delete or pay call before it runs (one extra call each). Default: unset (no review). review: auto # max_runtime_s: wall-clock cap in seconds for the whole run; the run stops with stop_reason timeout. Default: unset (no cap beyond the turn cap). max_runtime_s: 300 # budget_usd_run: per-run spend cap in USD, above 0 (blank = no cap; 0 is refused by the YAML block and Settings, and the runtime ignores a non-positive stored value); the loop stops at the next turn once cost passes it. Default: unset (no cap); a lower cap stored on the saved bot wins. budget_usd_run: 1 # permissions: per-class or per-tool presets for send, write, pay and delete; reads are allowed unless you name `read` or the tool. Default: allow, except the money floor which always asks. permissions: # permissions.send: preset for outward messages (email, chat, message_bot); ask pauses the run for the owner's approval. Default: allow, but a registry-agent call with operation send is always gated (ALWAYS_GATED) even with send: allow. send: ask # permissions.write: preset for calls that change something (files, schedule create, browser clicks). Default: allow. write: allow # permissions.pay: hand_off never runs the call and tells the owner it is theirs to do; the result ends with a [handed off to you] line. Default: allow, but payment ops always ask. pay: hand_off # permissions.delete: preset for destructive calls; deny refuses them. Default: allow. delete: deny # allowed_tools: the tool fence; only these tools reach the model and task_complete must be in it. Default: every tool of the runtime on agent one-agent (universal-agent's default is the 8-tool DEFAULT_FENCE); list tools to narrow it. allowed_tools: [web_search, web_fetch, memory, schedule, message_bot, browser, code, task_complete] # allowed_tools.schedule: the bot schedules itself (create, list, pause, resume, delete, run_now, wake); needs a saved bot. Default: available when allowed_tools is absent; name it to keep it in a narrowed fence. # allowed_tools.message_bot: sends to a peer or group: (async by default, replies arrive as [MESSAGE from ...] in a later run); needs a saved bot. Default: available when allowed_tools is absent; name it to keep it in a narrowed fence. # allowed_tools.browser: drives a cloud browser in one session, with the bot's saved login (click and type count as writes). Default: available when allowed_tools is absent; name it to keep it in a narrowed fence. # allowed_tools.memory: add, replace, remove, read, or forget {query} to purge matching entries. Default: available when allowed_tools is absent; name it to keep it in a narrowed fence. # allowed_tools.code: hands one turn (max 300 s) to the owner's OceanCode over the Local Tunnel; no daemon connected means no tool. Default: available when allowed_tools is absent; name it to keep it in a narrowed fence. # allowed_agents: the registry agents call_agent may reach (payment agents only when named). Default: all except payment agents. allowed_agents: [google-gmail] # skills: registry skill ids injected into the system prompt, at most 5. Default: none. skills: [web-search] # persona: one persona id that sets the voice. Default: none. persona: richard-feynman # max_turns: turn cap for the loop (max_iterations is the legacy spelling). Default: the effort preset's cap, else the runtime's own. max_turns: 8 # effort: quick, standard or deep sets the default turn cap and how wide the bot works. Default: unset. effort: quick ``` # Bot schedules and wake Source: https://www.plungeai.com/docs/bots/schedules-and-wake A bot runs when something starts it: you, a trigger, a chat message, or a schedule. A schedule is a scheduler job that runs the bot's saved workflow. You can create one from outside, or let the bot create its own. ## Schedule a bot from outside Over REST, `POST /v1/schedules` takes a saved workflow or bot id and either a cron expression or `@once` with `run_at`: ```bash 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":"","schedule":"0 9 * * 1-5","timezone":"America/Los_Angeles","parameters":{"deliver":["slack"]}}' ``` A one-time run removes itself after it fires: ```bash 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":"","schedule":"@once","run_at":"2026-10-06T09:00:00Z"}' ``` | Field | Meaning | |---|---| | `schedule` | A cron expression, or `@once` together with `run_at`. | | `run_at` | ISO-8601 time with a UTC offset, in the future. Required for `@once`. Refused with `run_at_required` or `run_at_past` otherwise. | | `timezone` | The IANA zone the cron is read in. Default UTC. | | `ends_at` | ISO-8601 time or ms epoch after which the schedule stops. `PATCH` with `ends_at: null` clears it. | | `parameters.deliver` | Where each result goes (below). | | `parameters.input` | The per-run input. | The same operations are on MCP (`plungeai_schedule`) and the CLI (`schedules create`, `--cron @once --at `, `--ends-at`); see [REST, MCP and CLI](/bots/rest-mcp-cli). ## Let the bot schedule itself List `schedule` in the bot's `allowed_tools`. The tool works on the bot's own jobs only: | Action | Input | Does | |---|---|---| | `create` | `name`, `cron`, `timezone?`, `prompt?`, `deliver?`, `ends_at?` | A recurring job. The model writes the 5-field cron and the reply echoes the schedule in words, so you can check it said what you asked. | | `create` with `cron: "@once"` | `run_at` | One run at a clock time. Refused without a UTC offset. | | `wake` | `in`, `note?` | One later run after a delay (`10m`, `2h`, `1d`; at least 60 s). The note becomes the run's input. | | `list` | | The bot's jobs with ids, the schedule in words and the next run. | | `pause`, `resume`, `delete` | `id` | By job id. | | `run_now` | `id`, `input?` | Fires a job once, now. | A bot holds at most 50 active jobs. `ends_at` retires a job after that time. With no `timezone` the job uses the user's zone, otherwise UTC. The class of a `schedule` call is by action: `delete` is `delete`, `list` is `read`, the rest are `write`. Under `posture: read_only` only `list` runs. ## Wake A wake re-runs the bot once, later, without touching its schedule. The bot calls `schedule` with `action: wake`; from outside, `POST /v1/schedules/{id}/wake` does the same: ```bash curl -s -X POST https://api.plungeai.com/v1/schedules//wake \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"note":"the inbox has new mail","in":"10m"}' ``` - `note` becomes the run's input. `in` is seconds or a string like `"90s"`, `"10m"`, `"2h"`; left out, the wake fires on the scheduler's next tick. - A pending wake is re-timed, never stacked, so repeated wakes leave one job. - A schedule with no saved workflow (a heartbeat) cannot be woken. Behind a wake is the scheduler's `runAgain`: it creates one `@once` job for the bot. ## One-time jobs An `@once` job fires once at its `run_at`, then deletes itself. A failed run may retry five minutes later first; the job is deleted on the final failure or on success. Because the job deletes itself, an [outbound hook](/bots/triggers-and-hooks#outbound-hooks) on it receives the run's last event and is then gone. ## Deliver the result `parameters.deliver` is a list of `{channel, chat_id?, to?}`. A bare channel name such as `"slack"` means the owner's own account. | Channel | Target | |---|---| | `inapp` | The Studio inbox. | | `email` | The owner's registered address; other addresses must be confirmed recipients. | | `slack`, `telegram`, `discord` | `chat_id`. | | `whatsapp` | `to` (a phone number) and the sending `phone_number_id`. | | `teams` | `chat_id`, a Microsoft Graph chat id. | Delivery is fenced by the owner's pairing and a per-channel length cap. Any other channel is `422 invalid_params` and nothing is created. - **`[SILENT]`:** when a run's result is exactly `[SILENT]`, or starts with it, the scheduler delivers to no channel and strips the marker from what it stores. A bot emits it when nothing is worth reporting. A run held for approval is never silent. - **Threads:** a delivery target may carry the reply or thread id of its channel, so the answer lands in the originating thread. - **Retries:** a failed delivery of a completed run is retried about 1 minute and then about 5 minutes later (three sends in all), then marked failed with an in-app notice. - **Local bots:** a job for a bot that runs on the owner's machine is skipped cleanly, with one notice, when that machine is offline. ## Run history `GET /v1/schedules/{id}/runs` lists the runs; `GET /v1/schedules/{id}` returns the schedule with its ten latest. A run has `status`, `started_at`, `ended_at`, `duration_ms`, `execution_id` and `error`. `POST /v1/schedules/{id}/run` runs it now and returns the `execution_id`, which `POST /v1/executions/{id}/cancel` accepts. `DELETE` removes the schedule and keeps its history readable. # Bot to bot, groups and A2A Source: https://www.plungeai.com/docs/bots/bot-to-bot-and-groups A bot reaches another bot with the `message_bot` tool. It can reach one bot, a group of your bots, or an agent outside the platform over A2A. Nothing is sent to a recipient the bot is not allowed to reach. ## message_bot List `message_bot` in `allowed_tools` and name the bots it may reach in `peers`: ```yaml - type: harness id: bot agent: one-agent goal: "{input}" peers: [Research Bot, "a2a:https://agents.example.com/"] allowed_tools: [message_bot, task_complete] ``` | Input | Meaning | |---|---| | `to` | A peer's name or id, `a2a:`, or `group:`. | | `text` | The message. It must say everything the receiver needs: the receiver does not see the sender's conversation. | | `wait` | `false` (default) queues the text and wakes the receiver. `true` runs the receiver now and returns its answer. | - **The fence:** `to` must be in `peers`, compared without regard to case. No `peers` list means no messaging. An `a2a:` URL must be listed as written. A group is the exception: it is the owner's own grant, so membership of the group is the fence. - **Queue and reply:** with `wait: false` the text lands in the receiver's inbox and the receiver wakes at once. Any reply arrives later as `[MESSAGE from ] ...` at the start of one of the sender's runs, so the sender finishes its own work first. - **`wait: true` is a plain run:** the receiver runs as a child (depth plus one) with no memory, no inbox, no bot workspace and no bot tools. Use `wait: false` to reach the full bot. - **Eight hops end a chain.** Each relay adds one; a message at the limit is refused, so two bots cannot loop forever. - A bot cannot message itself, and a name that matches two bots is refused rather than guessed. - Under `posture: read_only` the tool is denied (class `send`). `message_bot` results count as untrusted content. - Cancelling the sender cancels the child runs it started with `wait: true`. The inbox is readable by the owner: `GET /v1/workflows/{id}/inbox`. See [Workspace and inbox](/bots/workspace-and-inbox). ## Bot groups Create, replace and delete answer 503 `not_enabled` until the platform owner enables it; the API documents the code. A group is a named set of 2 to 6 of your own saved bots. It makes a team: one `message_bot` reaches all of them. - `name` is 1 to 40 letters, digits, `_`, `.` or `-`, starting with a letter, digit or `_`, and unique per account. - Members must all be your own bots, with no repeats. - Create and edit groups in Studio under Bot groups, over REST (`/v1/bot-groups`), with the CLI (`bot-groups`) or over MCP. ```bash curl -s -X POST https://api.plungeai.com/v1/bot-groups \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"name":"research-crew","members":["",""]}' ``` A bot in the group sends with `to: "group:research-crew"`: - the sender must be a member (`not a member of research-crew` otherwise), and `wait: true` is refused; - every other member's inbox gets the text as `[MESSAGE from in ]`, and the group's shared thread gets one entry; - only the member after the sender in the group's order wakes, wrapping round, so a group never runs all its members at once. Read the thread with `GET /v1/bot-groups/{id}/thread`, oldest first. It keeps the last 500 messages for 30 days. Deleting a group leaves its bots untouched. ## A2A: bots and agents outside The `a2a` worker speaks the Agent2Agent protocol (1.0, with 0.3 compatibility) in both directions. **An inbound door.** Any saved bot, agentic agent or workflow can have a door: `https://a2a.plungeai.com/`, with its own bearer token and agent card at `/.well-known/agent-card.json`. An outside A2A client's `SendMessage` runs the target as its owner and gets the final answer back as one text artifact. Reusing the `contextId` continues the conversation. A session that stops on a question answers `TASK_STATE_INPUT_REQUIRED`. - Mint, rotate, pause, resume and revoke a door in Studio under **Share, A2A**, or over MCP with `plungeai_workflow` and `action: a2a_door` (`door_action`: `mint`, `get`, `pause`, `resume`, `revoke`). - The token is shown once. Minting again rotates it. **An outbound call.** A `peers` entry `a2a:` lets `message_bot` reach any A2A agent. The bearer comes from your Connections credential `a2a:`, or `a2a:/` for one door on a host. The reply text is scanned as untrusted content, and the hop count travels in the message metadata with the same limit of eight. # Bot channels, voice and the chat widget Source: https://www.plungeai.com/docs/bots/channels-and-voice A bot can answer in the chats you already use. Every run started from a chat is a normal run of the bot, as its owner: it lands in your run history and stops at the bot's stored policy and spend caps. ## Pair a chat, bind it to a bot Bind and unbind answer 503 `not_enabled` until the platform owner enables them, and the bindings list is empty until then; the API documents the code. The channels are `telegram`, `whatsapp`, `discord` and `teams`; Slack is bound through the Slack app. Two steps: 1. **Pair the chat.** `POST /v1/channels/pair` with `{"channel": "telegram"}` returns a `code` valid for 10 minutes. Send it to the channel's bot from the chat (`/pair `). The chat then appears in `GET /v1/channels` with its `sender_id`. 2. **Bind it to a bot.** `POST /v1/workflows/{id}/bindings` with `{"channel": "telegram", "sender_id": ""}`. ```bash curl -s -X POST https://api.plungeai.com/v1/channels/pair \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"channel":"telegram"}' ``` - A chat talks to **one** bot: 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`. - `GET /v1/workflows/{id}/bindings` lists the chats bound to a bot; `DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}` unbinds one. - Studio has the same controls under the bot's channel bindings. The CLI has `workflows bindings list|bind|unbind`. You can also bind from inside the chat: `/bot ` talks to one of your bots there, and `/unbind` releases it. On Slack the commands are `/plunge bind ` and `/plunge unbind`. If a bot and a flow share a name, the bot wins. ### Address one bot for one message Start a message with `@` to run one bot on that message without changing the binding: `@research summarize this` runs the bot named `research` on `summarize this`, and the next plain message routes normally again. - The name must match one of your bots exactly, ignoring case. The `@` must be the first character. - Two bots with the same name are refused with a message, not guessed; no match answers `No bot named "" in your account.` - Works on Telegram, WhatsApp, Discord and Slack. ## Voice notes Voice notes work once the platform owner has deployed the updated message gateway, the same deploy as channel bindings. A voice note sent to a paired Telegram or WhatsApp chat is transcribed with Whisper, and the transcript is handled like typed text. - The audio is capped at 25 MB. A failed transcription answers with a fixed message asking you to type it or send it again. - The reply to a voice note comes back as **audio**: the first 4,000 characters are spoken (`tts-1`, voice `nova`, mp3). When the reply is longer, or when speech or the send fails, the text is sent too or instead, so a voice note is never left unanswered. - Typed messages are always answered in text. - **Discord and Teams are text-only.** - The paired owner's own OpenAI key is tried before the platform key. ## Microsoft Teams Teams has two parts. **Result delivery.** `parameters.deliver` takes `{"channel": "teams", "chat_id": ""}`, and the post is made as you, with your own Microsoft connection. With no Microsoft connection the delivery fails with `owner has no microsoft connection`; pairing is for inbound chats and is not what delivery needs. A channel target (`/`) is accepted by the format check but refused at delivery until the `ChannelMessage.Send` permission is granted. **The two-way chat bot.** Through the Bot Framework, the gateway verifies each inbound activity and replies to the conversation it came from. It is text only. The two-way Teams bot is off until the platform owner sets up its Azure bot registration, and `teams` cannot be paired before then. Result delivery does not need it. ## Channel security The gateway refuses a channel request it cannot verify. A missing secret, a missing or wrong signature header and any error inside the check all answer `401`; only a POST is processed. Telegram is verified by its secret-token header, WhatsApp by an `X-Hub-Signature-256` HMAC and Discord by an Ed25519 signature. ## Web chat widget A script tag puts a chat bubble for one bot on your own site. ```html ``` Creating or changing a widget answers 503 `widgets not enabled yet` until the platform owner enables it; the API documents the code. `embed.js` itself is always served. Create the widget in Studio, from the bot's menu, with these settings: | Setting | Meaning | |---|---| | Allowed origins | The sites that may frame the chat and call it. Only these origins are answered with CORS headers; never `*`. | | Enabled | Off answers `403`. | | Rate per minute | Messages per minute per widget, 1 to 600. Default 20. | | Greeting | The first line the visitor sees, up to 300 characters. | - The token is shown once, when the widget is created or rotated. Rotating stops the old snippet. - Each visitor message is one run of the bot as its owner. The text is only ever the run's `input`, and the run's memory is the bot's own, not yours. - The visitors spend your budget. Set a per-run spend cap on the bot first ([Permissions and budget](/bots/permissions-and-budget)); Studio warns when the bot has none. The per-run cap is the cost fence; the monthly cap only gates scheduled runs. - Rate limits apply per address and per widget, a message is at most 4,000 characters, and a conversation id (valid 24 hours) keeps a visitor's thread together. - The chat shell renders every reply as plain text, so a bot cannot inject markup into the page. # Bot permissions and budget Source: https://www.plungeai.com/docs/bots/permissions-and-budget A bot's rules live in two places that are merged for every run: the `permissions` map in its YAML, and a policy stored on the bot's row. The engine loads the stored policy once per run, so a run by id, a schedule, a trigger, a chat message, MCP, the CLI and Studio all stop at the same holds and caps. A shared or templated bot cannot unlock itself, because locks come from the row, never from the YAML. ## Classes and presets Every tool call has a class: `send`, `write`, `pay`, `delete` or `read`. A preset says what happens to a class (or to one tool name): | Preset | Effect | |---|---| | `allow` | The call runs. | | `ask` | The run pauses and waits for the owner. | | `deny` | The call does not run. | | `hand_off` | A deny that does not retry. The call never runs, and the result tells the owner it is theirs to do. Only One Agent understands it; any other runtime gets `deny`. | The stored policy covers `send`, `write`, `pay` and `delete`. In YAML, `permissions` can also name `read` and individual tools: ```yaml - type: harness id: bot agent: one-agent permissions: pay: hand_off send: ask budget_usd_run: 0.5 ``` How the order of checks works is on [Policy and review](/one-agent/policy-and-review). Two rules to remember: an operation named `buy`, `purchase`, `fetch_paid`, `send`, `transfer`, `pay`, `send_payment`, `withdraw` or `shop` is on the money floor and always `ask`, even with `send: allow`, and `posture: read_only` denies every mutating class whatever the presets say. ## Set the stored policy `PUT /v1/workflows/{id}/permissions` replaces the fields you send and leaves the others. It is owner-only. ```bash curl -s -X PUT https://api.plungeai.com/v1/workflows//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}' ``` | Field | Meaning | |---|---| | `classes` | A preset per class: `send`, `write`, `pay`, `delete`. 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`; it never lifts a `deny`. | | `budget_usd_run` | Cap on one run, in USD. A positive number sets it and `null` removes it; `0` is stored but means no cap. | | `budget_usd_month` | Cap on a month, in USD. A positive number sets it and `null` removes it; `0` is stored but means no cap. | `GET /v1/workflows/{id}/permissions` returns the stored policy. The same settings are in Studio under the bot's Permissions and Budget fields, on MCP, and on the CLI: ```bash ocean-cli workflows permissions set --class pay=ask --lock pay --budget-run 0.5 --budget-month 20 ``` - **Locks need a team admin** over the workflow: the project owner or an admin collaborator, or a company owner or admin. Anyone else gets `403 lock_admin_required`, and nothing in that body is written. - An invalid field is `422 invalid_params` and nothing is written. ## How the YAML and the row combine | Part | Rule | |---|---| | Class presets | The two are merged; the YAML's own entry wins per class. | | Locks | The row's locks replace whatever the YAML carried. | | `budget_usd_run` | The lower of the YAML's value and the row's wins. | ## Approvals A run that hits an `ask` pauses with status `needs_approval` and a `continuation` that carries the pending action, its price and its class. Answer it with `POST /v1/executions/{id}/continue`: ```bash curl -s -X POST https://api.plungeai.com/v1/executions//continue \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"approve":true,"allow_scope":"run"}' ``` | `allow_scope` | The approval reaches | |---|---| | `once` (default) | This call only. | | `run` | The same tool for the rest of the run. | | `always` | Not written by this call: it behaves as `once` for the run, and the next gated call asks again. The rule is the class preset stored with `PUT .../permissions`. Studio's "Allow always for this bot" stores the preset first and then approves; over REST, do the `PUT` yourself. | `{"answer": "..."}` replies to a question, or denies or redirects an approval. An unattended run that hits an `ask` waits for the owner, up to seven days. ## Spend caps - **Per run.** When the running token cost passes `budget_usd_run`, the run stops at the next turn with `stop_reason: budget`, and the result says `[run stopped at $... per-run budget cap after N turns ...]`. The engine applies it on every door. - **Per month.** `budget_usd_month` is enforced by the scheduler and gates scheduled runs only. A chat message or a web widget visitor is bounded by the per-run cap, which is why a bot behind a public surface needs one. # Bot workspace, inbox and memory Source: https://www.plungeai.com/docs/bots/workspace-and-inbox A run is short; a bot is not. Three things carry over from one run to the next: the files it writes, the messages other bots left for it, and what it remembers. ## Workspace files The file tools (`read_file`, `write_file`, `edit_file`, `list_files`, `search_files`, `delete_file`) work on a virtual workspace. The `workspace` key says how long it lives: | `workspace` | Files | |---|---| | `bot` | Kept across runs. The default for a saved bot. | | `run` | Start clean each run. | A bot that takes screenshots with `browser` finds them under `/shots/` in the same workspace. ### Read them `GET /v1/workflows/{id}/workspace-files` lists the persistent files, newest first, up to the 1,000 newest (`truncated` is `true` when there are more). `GET /v1/workflows/{id}/workspace-files/download?path=` returns one file as raw bytes with its stored content type and `Content-Disposition: attachment`. Both are owner-only. ```bash curl -s https://api.plungeai.com/v1/workflows//workspace-files \ -H "Authorization: Bearer $PLUNGE_API_KEY" curl -s -o report.md "https://api.plungeai.com/v1/workflows//workspace-files/download?path=notes/report.md" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` | Status and code | When | |---|---| | `400 invalid_path` | The path has a `..` segment, a backslash, a control character, an absolute path outside `/ws`, or an encoded form of any of these. | | `404 file_not_found` | The path is not a file the listing shows. | | `413 file_too_large` | The file is over 20 MB. | The CLI is `ocean-cli workflows files [--download --out ]`. A run's own deliverables (spreadsheets, documents, decks) are listed per execution by `GET /v1/executions/{id}/files`. ## Inbox When another bot sends a message with `message_bot`, the text is queued in the receiver's inbox and the receiver wakes. At the start of its next run the bot reads the queue as lines of the form `[MESSAGE from ] `, or `[MESSAGE from in ] ` for a group message, and the queue is cleared. - A message waits up to 7 days. - Eight relays end a chain, so two bots cannot loop forever. `GET /v1/workflows/{id}/inbox` shows what is queued, newest first. It is read-only and owner-only. See [Bot to bot and groups](/bots/bot-to-bot-and-groups). ## Memory A bot keeps durable facts with the `memory` tool and searches its past work with `recall` and `recall_history`. - **`memory`** applies a batch of `operations` (`add`, `replace`, `remove`) to the `user` or `memory` target in one call. `read` and `forget {query}` are single calls of their own; `forget` purges every entry containing the text. - **Whose memory.** A bot uses the memory of the user who runs it; `memory_owner` is refused at save. A bot's own namespace (`:bot:`) is wiped when the bot is deleted. - **Account memory over REST.** `GET /v1/memory/recall`, `POST /v1/memory/remember` and `POST /v1/memory/forget` read and write the facts every bot and agent you run can recall. `GET /v1/memory/runs?q=` searches your past runs (full text once the platform owner has enabled the index, a plain substring match until then), and `GET /v1/memory/runs/{id}` returns one stored result. - **Weekly consolidation (opt-in, default off).** With `memory_consolidate: weekly` in the bot's `bot-config` block, Studio creates a second job, ` · memory consolidation`, that runs on Sundays at 03:00. It replays the owner's recent runs through the memory review and records `reviewed N episodes`. With no key set there is no job, so nothing runs. ## Steering a running bot `POST /v1/executions/{id}/steer` queues a message the loop reads at its next turn. A finished run answers `409 execution_finished`. `POST /v1/executions/{id}/followup` messages a run in flight the same way, and starts a new turn once the run has finished. # Bot triggers and hooks Source: https://www.plungeai.com/docs/bots/triggers-and-hooks A trigger starts a bot from an event outside the platform. Every trigger hangs off a schedule of the bot (see [Schedules and wake](/bots/schedules-and-wake)), so the run goes through the same stored policy, budget and delivery as any other run. Hooks go the other way: they send the bot's run events to a URL you own. ## Inbound | Trigger | You get | Where to set it | |---|---|---| | Webhook | An https URL and a secret | REST, MCP, CLI | | Email | An address | REST, MCP, CLI | | Slack channel or keyword | A bot started by a message | Studio Triggers, Slack card | | Gmail new mail | A bot started once per new mail | Studio Triggers, Gmail card | | GitHub or GitLab event | A webhook preset with an event allow-list | Studio Triggers, Git card (the Studio route to a webhook) | The REST routes live under `/v1/triggers/{kind}`. Every call names the schedule with `schedule_id`: in the body for `POST`, in the query for `GET` and `DELETE`. Mint answers `201` with `Cache-Control: no-store`. **A secret is in the mint response once and is never listed again.** 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` mints a successor with the same settings and a new secret, then revokes the old token. ### Webhook ```bash 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":"","scheme":"github","prompt_template":"New PR: {pull_request.title}","filters":{"all":[{"path":"action","op":"equals","value":"opened"}]}}' ``` The response carries `token`, `secret`, `scheme` and the `url` to give the sender. | Field | Meaning | |---|---| | `scheme` | How the sender signs: `generic` (default, `X-Webhook-Signature` is the hex HMAC-SHA256 of the raw body), `github`, `gitlab` or `standard-webhooks`. | | `prompt_template` | The run's input. `{dotted.path}` fills in a field of the JSON payload. | | `filters` | `{all?, any?}` of rules `{path, op, value?}`, up to 20. `op` is `equals`, `contains`, `exists` or `in`. A path that starts with `headers.` reads a request header. | What the sender gets back, in order: | Answer | When | |---|---| | `401` | Bad signature, or a timestamp more than 300 seconds off. | | `409` | A delivery id seen in the last hour. | | `204` | The filters did not match. The delivery is acknowledged, no run starts and nothing is spent. | ### Email `POST /v1/triggers/email` with `{"schedule_id": "..."}` returns a `token` and an `address_local_part` of the form `bot+`; the address is that local part at the Email Routing domain your operator set up. Mail to it starts the schedule's bot. `allowed_from` (comma-separated addresses or domains) restricts the senders; it is a sender filter, weaker than a webhook signature. ### Slack channel or keyword Answers 503 `slack triggers not enabled yet` until the platform owner enables it; the API documents the code. In Studio, open the bot's Triggers and add a Slack trigger: a channel, and optionally a keyword. A human message in the channel that contains the keyword (the longest keyword wins; no keyword means every message) starts the bot with the input `from <@author> in <#channel>: text`. The run is signed as the trigger owner, so the bot's policy and the owner's budget apply. - App mentions, bot posts, edits and other subtypes never match, so there is no loop. Thread replies in the channel do fire. - Creating the trigger requires your own membership of the channel, checked server-side and fail-closed. - The channel can be added to the job's delivery, so the result lands there. - Anyone who can post in the channel spends the owner's budget: pick channels you trust. ### Gmail new mail In Studio, add a Gmail trigger: a Gmail search such as `from:billing@ subject:invoice` and a cadence of 5, 15 or 60 minutes. Studio saves a small check workflow and a heartbeat schedule. Each tick the scheduler runs the check as the owner, using the owner's own Google connection, and a YES starts the bot once per mail it has not started before, with the mail (From, Subject, Date, id and the plain-text body, up to 8,000 characters) as the run's input. - A mail starts at most one run. Only mail that arrived after the trigger was created counts, with at most 5 runs per tick. - A failed run is not retried, because a retry could repeat side effects. It shows in the bot's run history. - This is a poll: the cadence is the latency, and the YES or NO check costs one small model call per tick. A tick that finds no new mail puts nothing on the board. - Mail content is untrusted input. Choose searches you trust and keep the bot's tools narrow. ### GitHub and GitLab The Git card mints a `github` or `gitlab` webhook with an allow-list of events, then shows the URL and the secret once and where to paste them: in GitHub, repository Settings, Webhooks, Payload URL and Secret with `application/json`; in GitLab, Settings, Webhooks, URL and Secret token. Events outside the allow-list, including GitHub's `ping`, are acknowledged and ignored. ### Deliver-only A job with `parameters.deliver_only: true` never calls the engine or a model. When its trigger fires, the scheduler fills `parameters.template` (`{input}` is the incoming text) and posts it to the job's `deliver[]` targets, so a webhook can be forwarded to a channel at no model cost. The run history shows the filled text and no cost. - Slack and Discord pings and links in the incoming text (`@everyone`, ``, `<@U...>`, ``) are broken with an invisible character. Your own template is posted as written. - Other markup passes through, so put a `filters` allow-list on the trigger, and `allowed_from` on an email trigger. - A deliver-only job needs a non-empty `deliver[]` and a `template`. ## Outbound hooks Answers 503 `unavailable` until the platform owner enables it; the API documents the code. A hook sends the schedule's run events to an https URL you own. ```bash curl -s -X POST https://api.plungeai.com/v1/triggers/hooks \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"schedule_id":"","url":"https://example.com/ocean-hook","events":["run.finished","run.failed"]}' ``` The response carries the hook `id` and a `secret`, shown once. The URL must be https, public, with no credentials in it, and it is re-checked before every delivery. A schedule has at most 10 hooks. | Event | When | |---|---| | `run.started` | The run began. | | `run.finished` | The run completed. | | `run.failed` | The run failed, including an agent refusal that the history records as failed. | | `run.needs_approval` | The run stopped on a held action. | The body is `{event, delivery_id, timestamp, job_id, job_name, workflow_id, execution_id, run_id, status, result_summary?, error?}`. `execution_id` joins one run's events. Each delivery carries three headers: `X-Ocean-Delivery-Id`, `X-Ocean-Timestamp` (unix seconds) and `X-Ocean-Signature`, which is `sha256=` plus the hex HMAC-SHA256 of `..` under the secret. Recompute it over the raw body, compare in constant time, and reject an old timestamp: ```javascript import { createHmac, timingSafeEqual } from 'node:crypto' export function verify(secret, headers, rawBody) { const id = headers['x-ocean-delivery-id'] const ts = headers['x-ocean-timestamp'] if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false const want = 'sha256=' + createHmac('sha256', secret).update(`${id}.${ts}.${rawBody}`).digest('hex') const got = String(headers['x-ocean-signature'] ?? '') return got.length === want.length && timingSafeEqual(Buffer.from(got), Buffer.from(want)) } ``` - **Best effort.** One retry after a second, only on a network error, a timeout (10 seconds), a 5xx, a 408 or a 429. Any other answer is final. The retry sends the identical body, id and signature, so dedupe on `X-Ocean-Delivery-Id`. Redirects are never followed. A hook failure never fails or delays the run. - **Scope.** Scheduler-path runs only: cron, heartbeat, run now and triggers. An ad-hoc run from Studio or the API emits nothing. - **No ordering.** Events are sent as they happen, each independently. Order by `timestamp` and join on `execution_id`. - **One-time jobs.** An `@once` job deletes itself when its run ends, and its hooks with it. The run's last event is still delivered. # Control a bot over REST, MCP and the CLI Source: https://www.plungeai.com/docs/bots/rest-mcp-cli A bot is the same row whichever door you use. The One API, the MCP server and the Ocean CLI read and write the rows Studio shows, take the same `ozk_` key, and stop at the same stored policy. Pick the door that fits the caller: REST for code, MCP for an agent, the CLI for a terminal. | Door | Address | Auth | |---|---|---| | One API 2.3.0 | `https://api.plungeai.com` | `Authorization: Bearer $PLUNGE_API_KEY` | | MCP server 2.6.0 | `https://mcp.plungeai.com/v1` | The same key; tools are named `plungeai_*` | | Ocean CLI 2.1.0 | `ocean-cli` | `ocean-cli login`, or `OCEAN_API_KEY` | Another account's id answers the same `404 _not_found` as an id that does not exist. A storage or upstream failure is a generic `502`; the message never carries database text. Every error is `{"error": {"code", "message"}}` with an `X-Error-Code` header. The response format is yours to pick ([four wires](/api-reference/four-wires)). ## Operations by door | Do this | REST | MCP | CLI | |---|---|---|---| | Save a bot | `POST /v1/workflows` with `kind: bot` | `plungeai_workflow` `create` | `workflows create NAME FILE.yaml --kind bot` | | Check the YAML | The save runs the lint and answers `422 invalid_workflow` | The same, on `create` and `update` | `workflows validate FILE.yaml --kind bot` (local) | | Change it, version it | `PATCH /v1/workflows/{id}`, `POST .../versions` | `update`, `save_version`, `restore_version` | `workflows update`, `save-version`, `restore` | | Copy a template | `POST /v1/templates/{id}/use` | `plungeai_templates` | `templates` | | Read or set permissions and caps | `GET`, `PUT /v1/workflows/{id}/permissions` | `plungeai_workflow` `permissions_get`, `permissions_set` | `workflows permissions get`, `set` | | Run it | `POST /v1/workflows/{id}/execute` | `plungeai_execute_workflow` | `workflows run ID` | | Answer a pause or approve | `POST /v1/executions/{id}/continue` | `plungeai_continue` | `executions continue ID` | | Steer a running bot | `POST /v1/executions/{id}/steer` | `plungeai_executions` `steer` | `executions steer ID TEXT` | | List a run's files | `GET /v1/executions/{id}/files` | `plungeai_executions` `files` | `executions files ID` | | Schedule it | `POST /v1/schedules` (`run_at`, `ends_at`) | `plungeai_schedule` `create` | `schedules create` (`--cron @once --at`, `--ends-at`) | | Wake it | `POST /v1/schedules/{id}/wake` | `plungeai_schedule` `wake` | `schedules wake ID --note TEXT` | | Add a trigger or hook | `POST /v1/triggers/{webhook,email,hooks}` | `plungeai_schedule` `webhook_mint`, `email_mint`, `hooks_mint` (and `_list`, `_delete`) | `schedules triggers webhook\|email\|hooks mint\|list\|delete` | | Read its workspace | `GET /v1/workflows/{id}/workspace-files`, `.../download?path=` | `plungeai_workflow` `files`, `file` | `workflows files ID [--download PATH --out FILE]` | | Read its inbox | `GET /v1/workflows/{id}/inbox` | `plungeai_workflow` `inbox` | `workflows inbox ID` | | Bind a chat | `POST /v1/workflows/{id}/bindings`, `DELETE .../{channel}/{sender_id}` | `plungeai_workflow` `bindings`, `bind`, `unbind` | `workflows bindings list\|bind\|unbind` | | Pair a chat | `POST /v1/channels/pair`, `GET /v1/channels`; then send the code from the chat | Studio or REST | Studio or REST | | Manage bot groups | `/v1/bot-groups`, `.../{id}/thread` | `plungeai_workflow` `groups`, `group_set`, `group_delete`, `group_thread` | `bot-groups list\|create\|update\|delete\|thread` | | Give it an A2A door | No REST route; Studio (Share, A2A) | `plungeai_workflow` `a2a_door` | Studio (Share, A2A) | | Search its runs, its memory | `GET /v1/memory/runs`, `/v1/memory/recall` | `plungeai_memory` | `memory` | The operation pages are in the API reference: [Bots](/api-reference/bots/get-bot-permissions), [Schedules](/api-reference/schedules/create-schedule), [Triggers](/api-reference/triggers/create-webhook-trigger) and [Executions](/api-reference/executions/steer-execution). ## The agent call door `POST /v1/execute` takes the program as the body and always runs through the engine, so a saved bot keeps its stored policy. The body is JSON or YAML: | Body | Runs | |---|---| | `agent: ` and `input` | Your saved agent or bot, from its live YAML. | | `agent: @3` and `input` | Snapshot 3 of it: a pinned version. | | `agent: ` and that agent's own fields | A registry agent as a one-task workflow. | | `workflow:`, or a root `name` and `tasks:` | An inline workflow. | `dry_run: true` validates and reports what would run without running it, spending anything or writing an execution row. `stream: true` answers as server-sent events. See [Agent call](/api-reference/agent-call/agent-call). ## MCP - Every tool takes `user_request` (the user's words) and most take `format`: `markdown` (default) or `json`. A tool that runs something also takes `user_context` (time zone, locale, place), merged over the zone the network request implies. - `plungeai_get_result` with `format: json` returns the structured result (`status`, `result`, `tasks`), not the rendered thread. - `plungeai_workflow` `create` takes `kind`: `workflow`, `agent`, `bot` or `campaign`. - `plungeai_schedule` `create` with `schedule: "@once"` needs `run_at`: without it the answer is `run_at required`. - `permissions_set` takes `classes`, `locks`, `budget_usd_run` and `budget_usd_month`; `locks` needs a team admin. - `file` returns a workspace file as base64, up to 5 MiB; a bigger file is refused with the REST download route to use instead. ## CLI The verbs below are in Ocean CLI 2.1.0. Run it from the repository today with `npm run ocean-cli -- `; once it is published it installs with `npm install -g @plungeai/ocean` and runs as `ocean-cli`. - Every verb takes `--json` (one JSON document on stdout) and, when it creates, runs or pays, `--dry-run` (prints the exact request and sends nothing). - Destructive verbs (delete, unbind, restore, forget) need `--yes`. - `workflows permissions set` reads the stored classes and locks first and sends only what you name, so one `--class` keeps the others. - Exit codes: 2 bad input, 3 auth, 4 API error or a run that needs input or approval, 5 timeout. ```bash ocean-cli workflows permissions set --class pay=ask --lock pay --budget-run 0.5 --budget-month 20 ocean-cli schedules create "Reminder" --type workflow --target --cron @once --at 2026-10-06T09:00:00Z ocean-cli bot-groups create --name research-crew --members , ocean-cli executions steer "also check the pricing page" ``` # Companies Source: https://www.plungeai.com/docs/bots/companies A company is a **campus**. You write it as YAML documents of two kinds and save them like any workflow. A `kind: campus` document names the buildings, and a `kind: building` document (or a building written inline in the campus) names its floors of offices, the standard its policy follows, how items come in, and the pipeline an item walks. An office is an ordinary workflow: the agents doing the work. | Part | What it is | |---|---| | Campus | the company: a slug, a mode and its buildings in map order | | Building | offices on floors, a policy (`code` or `action` standard), intake and a pipeline. `location: cloud` runs on the platform, and `location: local` runs on your own machine | | Office | a saved workflow, or one written inline that apply saves as `//` | | Item | one piece of work put into a building; it walks the pipeline step by step | | Gate | a pipeline step where the mode and the policy decide: pass, or ask a person (an approval) | The mode decides who passes a gate. `supervised` asks a person at every gate. `tiered` asks according to the standard's tiers. `autonomous` asks only where the policy says `require`. ## The lifecycle 1. Save the documents with [`POST /v1/workflows`](/api-reference/workflows/create-workflow) and `kind: campus` or `kind: building`. 2. [Apply](/api-reference/companies/apply-company): compile them into the stored plan. 3. [Start the clock](/api-reference/companies/start-company): cloud buildings begin walking their items. 4. [Put items in](/api-reference/companies/create-company-item) and watch the [snapshot](/api-reference/companies/get-company-snapshot) or the [event feed](/api-reference/companies/list-company-events). 5. Decide the [approvals](/api-reference/companies/list-company-approvals) waiting at the door: [approve or reject](/api-reference/companies/decide-company-approval). 6. [Stop the clock](/api-reference/companies/stop-company), then [delete](/api-reference/companies/delete-company) the company. Delete removes every company row. Your saved documents stay until you delete them. Every route is in the API reference under **Companies**. The One API guide's Companies chapter (`api.plungeai.com/docs`, chapter 17) walks the whole lifecycle with live responses. The errors are `404 company_not_found` for a campus that is not yours, `409 conflict` for deleting a running company, and `422 invalid_params` for a body that breaks a rule. # Developer Tools Overview Source: https://www.plungeai.com/docs/developer-tools/quickstart Planned: not available yet. Tracked as TI-74. ## What this will do MCP vs One API vs CLI vs Studio: pick your door. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Account API Source: https://www.plungeai.com/docs/developer-tools/account-api Planned: not available yet. Tracked as TI-06. ## What this will do A key-authenticated Account API will let a script manage keys, the wallet and usage without the Dashboard. How that API authenticates is still open. Today every account action is a Dashboard view; the table below lists where each one lives. ## Available today in the Dashboard | What it does | Dashboard location | |---|---| | Create, list and revoke `ozk_` keys (legacy `sk-ocean-` and `sk-conn-` keys: list and revoke only) | **Dashboard → One API → Keys** | | Wallet balance, top-up, auto-recharge, BYOK provider keys | **Dashboard → One API → Billing** | | Ledger of top-ups and charges | **Dashboard → Billing → Ledger** | | Usage by user, workflow, model or channel | **Dashboard → Billing → Usage** | | Request log of One API calls | **Dashboard → One API → Requests** | | Model and connector catalog (provider cost basis) | **Dashboard → One API → Catalog** | | Connected accounts | **Dashboard → Secrets & Connections → Connections** | | Teams, companies and roles | **Dashboard → Team** and **Dashboard → Workspace** | | Who am I (identity, tier, rate window) | Key-authenticated today over MCP: [plungeai_whoami](/account-api/identity/whoami) | ## Use this today Create and revoke keys in the Dashboard. Where the wallet balance lives today. # Install PlungeAI Agent Skills Source: https://www.plungeai.com/docs/developer-tools/agent-skills Planned: not available yet. Tracked as TI-74. ## What this will do 31 skills that teach coding agents to use PlungeAI. This page is not written yet; the source below covers the same material today. ## Use this today All 31 agent skills with their install options. # Anthropic Tool Calling Source: https://www.plungeai.com/docs/developer-tools/anthropic-tool-calling Planned: not available yet. Tracked as TI-74. ## What this will do Turn PlungeAI tool contracts into Claude tool definitions. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Claude Code Plugin Source: https://www.plungeai.com/docs/developer-tools/claude-code-plugin Planned: not available yet. Tracked as TI-74. ## What this will do The `plungeai` plugin: 31 skills plus its own MCP server config. Install it instead of `claude mcp add`, not in addition. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Ocean CLI Source: https://www.plungeai.com/docs/developer-tools/cli Planned: not available yet. Tracked as TI-74. ## What this will do The `ocean` command: auth, runs, workflows, schedules, registry. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # Cursor Plugin Source: https://www.plungeai.com/docs/developer-tools/cursor-plugin Planned: not available yet. Tracked as TI-13. ## What this will do A Cursor plugin will install the PlungeAI MCP server and skills from the Cursor marketplace in one step. No Cursor plugin exists today; Cursor connects through its MCP config. ## Use this today Connect Cursor to `https://mcp.plungeai.com/v1` with the one-click install. # LangChain Source: https://www.plungeai.com/docs/developer-tools/langchain Planned: not available yet. Tracked as TI-74. ## What this will do Use the models plane from LangChain's `ChatOpenAI`. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # PlungeAI MCP Server Quickstart Source: https://www.plungeai.com/docs/developer-tools/mcp/quickstart The PlungeAI MCP server gives any MCP client the 20 `plungeai_*` tools: discover capabilities, run tools, agents, workflows and missions, schedule jobs and read results. ## When to use PlungeAI MCP? All four surfaces use the same `ozk_` key, the same account and the same data. | Surface | What it is | Use it when | |---|---|---| | MCP server, `https://mcp.plungeai.com/v1` | The 20 `plungeai_*` tools over MCP | An AI client operates PlungeAI live: discover, run, save, schedule, remember, with approvals and continuations | | One API, `https://api.plungeai.com` | REST routes, OpenAPI at `GET /v1/openapi.json` | You write or generate code that calls PlungeAI | | [Skills](/skills), zips at `https://skills.plungeai.com` | 31 instruction packs your AI client loads | Your client should already know PlungeAI's conventions; skills complement a connection | | Ocean Studio, `https://studio.plungeai.com` | The browser app | A person builds visually, connects accounts and reviews runs | A docs MCP server that searches and reads these pages from your agent is planned, tracked as TI-35: see [Docs MCP](/developer-tools/mcp/docs-mcp). ## Endpoint | Item | Value | |---|---| | URL | `https://mcp.plungeai.com/v1` | | Transport | Streamable HTTP, JSON-RPC 2.0 over `POST` | | Authentication | `Authorization: Bearer $PLUNGE_API_KEY` or `X-API-Key: $PLUNGE_API_KEY` (an `ozk_` key); every request needs it, `tools/list` included | | Responses | JSON; `tools/call` streams as server-sent events | Create the key at **Dashboard → One API → Keys** (https://dashboard.plungeai.com/one-api?tab=keys) and export it as `PLUNGE_API_KEY` before you configure a client. ## Quick installation ### Let your agent install it Paste this prompt into your coding agent; it reads the setup file and connects itself: ```text Onboard from /agents.md Use curl to read https://www.plungeai.com/docs/agents.md and perform the setup to connect PlungeAI ``` Or name the server directly: ```text Add the PlungeAI MCP server Add the MCP server https://mcp.plungeai.com/v1 named plungeai to my user-level client config, with the header Authorization: Bearer and the value of my PLUNGE_API_KEY environment variable, then call plungeai_whoami to confirm the connection. ``` ### Client installation Claude Code, one command: ```bash claude mcp add --transport http plungeai https://mcp.plungeai.com/v1 \ --header "Authorization: Bearer $PLUNGE_API_KEY" --scope user ``` Codex, which reads the key from `PLUNGEAI_API_KEY`: ```bash export PLUNGEAI_API_KEY="$PLUNGE_API_KEY" codex mcp add plungeai --url https://mcp.plungeai.com/v1 --bearer-token-env-var PLUNGEAI_API_KEY ``` A project `.mcp.json` that reads the key from each developer's environment, safe to commit: ```json { "mcpServers": { "plungeai": { "type": "http", "url": "https://mcp.plungeai.com/v1", "headers": { "Authorization": "Bearer ${PLUNGE_API_KEY}" } } } } ``` One-click installs open your client's install dialog with the URL filled in; add the key afterwards in the client's user-level config: Opens Cursor's MCP install dialog. Opens VS Code's MCP install dialog. Adds the server to Replit Agent. Step-by-step pages per client: User-level `~/.cursor/mcp.json`. Copilot agent mode. Integrations → MCP Servers. Verify in any client: ask it to use plungeai_whoami to confirm your identity. These docs use PLUNGE_API_KEY; the PlungeAI skills, the Claude Code plugin and the install page use PLUNGEAI_API_KEY; any name works if your config and shell agree. # Programmatic use Source: https://www.plungeai.com/docs/developer-tools/mcp/programmatic-use Planned: not available yet. Tracked as TI-74. ## What this will do Call the MCP server from your own code with raw JSON-RPC, and run outbound MCP servers with `/v1/mcp/runs`. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # PlungeAI MCP Source: https://www.plungeai.com/docs/developer-tools/mcp/plungeai-mcp Planned: not available yet. Tracked as TI-74. ## What this will do The one server: install per client, configure, troubleshoot. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Workflows and missions over MCP Source: https://www.plungeai.com/docs/developer-tools/mcp/workflows-mcp Planned: not available yet. Tracked as TI-74. ## What this will do Run workflows and missions from your coding agent, sync or async. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Protocol and transport Source: https://www.plungeai.com/docs/developer-tools/mcp/protocol Planned: not available yet. Tracked as TI-74. ## What this will do Streamable HTTP, JSON-RPC methods, sessions, SSE progress. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Prompts and resources Source: https://www.plungeai.com/docs/developer-tools/mcp/prompts-resources Planned: not available yet. Tracked as TI-74. ## What this will do The `/plungeai` prompt and every `plungeai://` resource. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Move MCP clients from /mcp to /v1 Source: https://www.plungeai.com/docs/developer-tools/mcp/migrate-to-v1 Planned: not available yet. Tracked as TI-74. ## What this will do `/mcp` is a legacy alias since 2026-09-20; configure `/v1`. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Docs MCP server Source: https://www.plungeai.com/docs/developer-tools/mcp/docs-mcp Planned: not available yet. Tracked as TI-35. ## What this will do The docs at www.plungeai.com/docs will run their own MCP server so an agent can search these pages and read any of them as markdown. It is a separate server from the PlungeAI MCP server, and it is not running yet. ## Use this today Read these docs as markdown today: llms.txt, llms-full.txt and a .md twin of every page. # OAuth Provider Source: https://www.plungeai.com/docs/developer-tools/oauth-provider Planned: not available yet. Tracked as TI-05. ## What this will do MCP clients that connect only through OAuth will be able to sign in with a PlungeAI account instead of sending a key. Today the MCP server accepts bearer `ozk_` keys only. ## Use this today Connect any MCP client with an ozk_ key in the Authorization header. # Ollama Tool Calling Source: https://www.plungeai.com/docs/developer-tools/ollama-tool-calling Planned: not available yet. Tracked as TI-74. ## What this will do Turn PlungeAI tool contracts into tool definitions for a local Ollama model through its OpenAI-compatible endpoint. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # OpenAI SDK Source: https://www.plungeai.com/docs/developer-tools/openai-sdk Planned: not available yet. Tracked as TI-74. ## What this will do Point the OpenAI Python or TypeScript SDK at PlungeAI. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # OpenAI Tool Calling Source: https://www.plungeai.com/docs/developer-tools/openai-tool-calling Planned: not available yet. Tracked as TI-74. ## What this will do Turn tool contracts into OpenAI function definitions and execute the calls. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Generate a client from OpenAPI Source: https://www.plungeai.com/docs/developer-tools/openapi-codegen Planned: not available yet. Tracked as TI-74. ## What this will do Typed clients from `/v1/openapi.json`. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill reference that covers the same ground. # OpenCode Plugin Source: https://www.plungeai.com/docs/developer-tools/opencode-plugin Planned: not available yet. Tracked as TI-74. ## What this will do Connect OpenCode today with an MCP config; a plugin is planned. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # @plungeai/one-api Source: https://www.plungeai.com/docs/sdks/typescript The official TypeScript SDK for the PlungeAI One API (`https://api.plungeai.com`). Targets One API `2.5.0`; package version `1.0.0`. The resource methods and their types are generated from the API's own OpenAPI document (`GET /v1/openapi.json`). Zero runtime dependencies. Release status: `npm view @plungeai/one-api version`; until it answers, build from `sdk/one-api-client/` (`npm ci && npm run build`). ## Installation ```sh npm install @plungeai/one-api ``` ## Usage ```ts import PlungeAI from '@plungeai/one-api' const client = new PlungeAI({ apiKey: process.env['PLUNGEAI_API_KEY'], // the default; can be omitted }) const me = await client.whoami() const run = await client.agents.execute('calculator-agent', { prompt: '6*7' }) const hits = await client.discovery.search({ q: 'web search', kind: 'agents' }) ``` ## Streaming responses ```ts const stream = await client.workflows.executeInlineStream({ workflow }) for await (const event of stream) { if (event.event === 'token') process.stdout.write((event.data as { delta: string }).delta) } ``` `break` out of the loop or call `stream.controller.abort()` to stop; either aborts the underlying request. `stream.tee()` splits a stream in two, `stream.toReadableStream()` hands it on as a web `ReadableStream`. Methods that take `stream: true` (`client.agents.execute`, `client.chat.completions.create`) return the same `Stream`. ## Auto-pagination ```ts for await (const w of client.workflows.list({ kind: 'bot' })) { console.log(w) } let page = await client.workflows.list({ limit: 50 }) for (const w of page.data) console.log(w) while (page.hasNextPage()) { page = await page.getNextPage() for (const w of page.data) console.log(w) } ``` ## Handling errors ```ts try { await client.agents.execute('calculator-agent', { prompt: '6*7' }) } catch (err) { if (err instanceof PlungeAI.FailedDependencyError) console.log(err.connect?.link) else if (err instanceof PlungeAI.APIError) console.log(err.status, err.code, err.requestID) else throw err } ``` | Status | Error class | |---|---| | 400 | `BadRequestError` | | 401 | `AuthenticationError` | | 403 | `PermissionDeniedError` | | 404 | `NotFoundError` | | 409 | `ConflictError` | | 422 | `UnprocessableEntityError` | | 424 | `FailedDependencyError` — `connect: { provider, method, link }` says where to connect the missing account | | 429 | `RateLimitError` | | ≥ 500 | `InternalServerError` | | no response | `APIConnectionError` (`APIConnectionTimeoutError` on timeout) | Every class is exported by name and as a static on `PlungeAI`. `err.code` is the envelope's `error.code`; `err.requestID` is the `x-request-id` header. ## Retries Failed requests are retried 2 times by default with short exponential backoff (honouring `Retry-After`): connection errors, timeouts, 408, 429 and ≥ 500. 409 and 424 are never retried — on the One API they are semantic answers (`approval_required`, `duplicate_execution_id`, a missing connection), and a retried execute would start a second run. Set `maxRetries` on the client or per request: ```ts const client = new PlungeAI({ maxRetries: 0 }) await client.agents.execute('calculator-agent', { prompt: '6*7' }, { maxRetries: 5 }) ``` `idempotencyKey` (per request) is sent as `x-trace-id`, the API's idempotency key: it becomes the run's execution id, is re-sent on every retry of the call, and a replay answers 409 `duplicate_execution_id`. ## Timeouts Requests time out after 10 minutes by default and throw `APIConnectionTimeoutError` (retried as above). Set `timeout` in milliseconds on the client or per request: `new PlungeAI({ timeout: 20_000 })`, `client.whoami({ timeout: 5_000 })`. ## Models plane `client.chat.completions.create` (plus `client.embeddings` and `client.models`) covers the OpenAI-compatible routes here; the OpenAI SDK also works with `baseURL: 'https://api.plungeai.com/v1'` and the same key. ## Requirements Node.js 20+, Deno, Bun, Cloudflare Workers and Vercel Edge. Browsers are refused unless you pass `dangerouslyAllowBrowser: true`, because a browser client exposes your `ozk_` key to anyone who loads the page. # plungeai (Python) Source: https://www.plungeai.com/docs/sdks/python The official Python SDK for the PlungeAI One API (`https://api.plungeai.com`). Targets One API `2.5.0`; package version `1.0.0`. The resource methods and their types are generated from the API's own OpenAPI document (`GET /v1/openapi.json`). One runtime dependency: `httpx`. Release status: `pip index versions plungeai`; until it answers, build from `sdk/one-api-python/` (`bash scripts/build.sh`, then `pip install target/sdk-py-build/*.whl`). ## Installation ```sh pip install plungeai ``` ## Usage ```python from plungeai import PlungeAI client = PlungeAI() # reads PLUNGEAI_API_KEY; or PlungeAI(api_key="ozk_…") me = client.whoami() run = client.agents.execute("calculator-agent", prompt="6*7") hits = client.discovery.search(q="web search", kind="agents") ``` Method names are `snake_case`; request fields are keyword arguments typed as `TypedDict`s (`plungeai.types`); responses are plain `dict`s typed the same way. Path parameters come first, positionally: `client.workflows.versions.restore(workflow_id, version)`. ## Async client ```python import asyncio from plungeai import AsyncPlungeAI async def main() -> None: client = AsyncPlungeAI() run = await client.agents.execute("calculator-agent", prompt="6*7") async for event in await client.workflows.execute_inline_stream(workflow=workflow): print(event.event, event.data) asyncio.run(main()) ``` Every method exists on both clients with the same signature. ## Streaming responses ```python stream = client.workflows.execute_inline_stream(workflow=workflow) for event in stream: if event.event == "token": print(event.json()["delta"], end="") ``` `event.data` is the raw string of the server-sent event; `event.json()` parses it. Leaving the loop closes the underlying response; on the async client an `AsyncStream` closes on `[DONE]`, end of body or an error, so pair an early `break` with `async with await client.agents.execute(..., stream=True) as stream:` (or `await stream.aclose()`). Dict and list query values are sent as JSON. Methods that take `stream=True` (`client.agents.execute`, `client.chat.completions.create`) return the same `Stream`. ## Auto-pagination ```python for w in client.workflows.list(kind="bot"): print(w) page = client.workflows.list(limit=50) for w in page.data: print(w) while page.has_next_page(): page = page.get_next_page() for w in page.data: print(w) ``` ## Handling errors ```python from plungeai import APIError, FailedDependencyError try: client.agents.execute("calculator-agent", prompt="6*7") except FailedDependencyError as err: print(err.connect["link"]) except APIError as err: print(err.status_code, err.code, err.request_id) ``` | Status | Error class | |---|---| | 400 | `BadRequestError` | | 401 | `AuthenticationError` | | 403 | `PermissionDeniedError` | | 404 | `NotFoundError` | | 409 | `ConflictError` | | 422 | `UnprocessableEntityError` | | 424 | `FailedDependencyError` — `connect: { provider, method, link }` says where to connect the missing account | | 429 | `RateLimitError` | | ≥ 500 | `InternalServerError` | | no response | `APIConnectionError` (`APITimeoutError` on timeout) | Every class is importable from `plungeai`. `err.code` is the envelope's `error.code`; `err.request_id` is the `x-request-id` header. ## Retries Failed requests are retried 2 times by default with short exponential backoff (honouring `Retry-After`): connection errors, timeouts, 408, 429 and ≥ 500. 409 and 424 are never retried — on the One API they are semantic answers (`approval_required`, `duplicate_execution_id`, a missing connection), and a retried execute would start a second run. Set `max_retries` on the client or per call: ```python client = PlungeAI(max_retries=0) client.agents.execute("calculator-agent", prompt="6*7", max_retries=5) ``` `idempotency_key` (per call) is sent as `x-trace-id`, the API's idempotency key: it becomes the run's execution id, is re-sent on every retry of the call, and a replay answers 409 `duplicate_execution_id`. ## Timeouts Requests time out after 10 minutes by default and raise `APITimeoutError` (retried as above). Set `timeout` in seconds on the client or per call: `PlungeAI(timeout=20.0)`, `client.whoami(timeout=5.0)`. ## Raw responses ```python response = client.agents.with_raw_response.execute("calculator-agent", prompt="6*7") print(response.status_code, response.headers["x-request-id"]) run = response.parse() ``` `extra_headers` and `extra_query` on any call add to the request; `client.get(path, query=…)`, `client.post(path, body=…)` and friends reach routes the generated layer does not cover; `executions.continue` is spelled `client.executions.continue_(...)` (Python keyword). ## Models plane `client.chat.completions.create(...)` (plus `client.embeddings` and `client.models`) covers the OpenAI-compatible routes here; the OpenAI SDK also works with `base_url="https://api.plungeai.com/v1"` and the same key. ## Requirements Python 3.10 or newer. Type-checked with `mypy --strict`; the package ships `py.typed`. # plungeai-go Source: https://www.plungeai.com/docs/sdks/go The official Go SDK for the PlungeAI One API (`https://api.plungeai.com`). Targets One API `2.5.0`; module version `1.0.0`. The resource methods and their types are generated from the API's own OpenAPI document (`GET /v1/openapi.json`). Standard library only. Release status: `go list -m -versions github.com/PlungeAI/plungeai-go`; until it answers, use a `replace` directive to `sdk/one-api-go/` in the platform repository. ## Installation ```sh go get github.com/PlungeAI/plungeai-go ``` ## Usage ```go import "github.com/PlungeAI/plungeai-go" client := plungeai.NewClient() // reads PLUNGEAI_API_KEY; or plungeai.NewClient(plungeai.WithAPIKey("ozk_…")) ctx := context.Background() me, err := client.Whoami(ctx) run, err := client.Agents.Execute(ctx, "calculator-agent", plungeai.AgentExecuteParams{Prompt: "6*7"}) hits, err := client.Discovery.Search(ctx, plungeai.DiscoverySearchParams{Q: "web search", Kind: "agents"}) ``` Every method takes a `context.Context` first, then the path parameters, then a params struct (`plungeai.Params`), then optional request options. Optional bool and number request fields are pointers (`plungeai.Bool(false)`, `plungeai.Int(50)`, `plungeai.Float(0.2)`) so `false` and `0` reach the wire; optional strings are plain. Responses are pointers to generated structs; nested resources are fields (`client.Workflows.Versions.List(ctx, workflowID)`). ## Streaming responses ```go stream, err := client.Workflows.ExecuteInlineStream(ctx, plungeai.WorkflowExecuteInlineStreamParams{Workflow: workflow}) if err != nil { return err } defer stream.Close() for stream.Next() { ev := stream.Current() if ev.Event == "token" { var token struct{ Delta string `json:"delta"` } _ = ev.JSON(&token) fmt.Print(token.Delta) } } if err := stream.Err(); err != nil { return err } ``` `ev.Data` is the raw string of the server-sent event; `ev.JSON(&v)` decodes it. Cancelling `ctx` stops the stream. Methods that accept `stream: true` on the API have a `…Streaming` twin (`client.Agents.ExecuteStreaming`, `client.Chat.Completions.CreateStreaming`) returning the same `*Stream`. ## Auto-pagination ```go pager := client.Workflows.ListAutoPaging(ctx, plungeai.WorkflowListParams{Kind: "bot"}) for pager.Next() { fmt.Println(pager.Current()) } if err := pager.Err(); err != nil { return err } page, err := client.Workflows.List(ctx, plungeai.WorkflowListParams{Limit: plungeai.Int(50)}) for err == nil { for _, w := range page.Data { fmt.Println(w) } if !page.HasNextPage() { break } page, err = page.GetNextPage(ctx) } ``` ## Handling errors ```go _, err := client.Agents.Execute(ctx, "calculator-agent", plungeai.AgentExecuteParams{Prompt: "6*7"}) var apiErr *plungeai.Error if errors.As(err, &apiErr) { switch apiErr.StatusCode { case 424: fmt.Println(apiErr.Connect.Link) default: fmt.Println(apiErr.StatusCode, apiErr.Code, apiErr.RequestID) } } ``` One error type, `*plungeai.Error`, carries `StatusCode`, `Code` (the envelope's `error.code`), `Message`, `RequestID` (the `x-request-id` header) and, on 424, `Connect` (`Provider`, `Method`, `Link` — where to connect the missing account). A request that never got a response returns the transport error (`errors.Is(err, context.DeadlineExceeded)` on timeout); a missing key returns `plungeai.ErrMissingAPIKey`. ## Retries Failed requests are retried 2 times by default with short exponential backoff (honouring `Retry-After`): connection errors, timeouts, 408, 429 and ≥ 500. 409 and 424 are never retried — on the One API they are semantic answers (`approval_required`, `duplicate_execution_id`, a missing connection), and a retried execute would start a second run. Set the count on the client or per call: ```go client := plungeai.NewClient(plungeai.WithMaxRetries(0)) client.Agents.Execute(ctx, "calculator-agent", plungeai.AgentExecuteParams{Prompt: "6*7"}, plungeai.WithMaxRetries(5)) ``` `plungeai.WithIdempotencyKey(key)` is sent as `x-trace-id`, the API's idempotency key: it becomes the run's execution id, is re-sent on every retry of the call, and a replay answers 409 `duplicate_execution_id`. ## Timeouts Each attempt times out after 10 minutes by default (retried as above). Set it on the client or per call: `plungeai.WithRequestTimeout(20 * time.Second)`. A deadline on `ctx` bounds the whole call, retries included. ## Raw responses ```go var raw *http.Response run, err := client.Agents.Execute(ctx, "calculator-agent", plungeai.AgentExecuteParams{Prompt: "6*7"}, plungeai.WithResponseInto(&raw)) fmt.Println(raw.StatusCode, raw.Header.Get("x-request-id")) ``` `plungeai.WithHeader(name, value)` and `plungeai.WithQuery(name, value)` add to any request (a `WithQuery` value overrides a same-named params field, as in the TypeScript SDK); `client.Get(ctx, path, params, &out)`, `client.Post(ctx, path, body, &out)` and friends reach routes the generated layer does not cover; the root `POST /v1/execute` is `client.ExecuteCall` (the name `Execute` is the generic request method). ## Models plane `client.Chat.Completions.Create` (plus `client.Embeddings` and `client.Models`) covers the OpenAI-compatible routes here; any OpenAI client also works with base URL `https://api.plungeai.com/v1` and the same key. ## Requirements Go 1.24 or newer. The module is published from the public repository `github.com/PlungeAI/plungeai-go`; its source of truth is `sdk/one-api-go/` in the platform repository. # plungeai (Rust) Source: https://www.plungeai.com/docs/sdks/rust The official Rust SDK for the PlungeAI One API (`https://api.plungeai.com`). Targets One API `2.5.0`; crate version `1.0.0`. The resource methods and their types are generated from the API's own OpenAPI document (`GET /v1/openapi.json`). Async over `reqwest` (rustls) on the `tokio` runtime. Release status: `cargo search plungeai`; until it answers, depend on the path `sdk/one-api-rust/` in the platform repository. ## Installation ```sh cargo add plungeai tokio --features tokio/full ``` ## Usage ```rust use plungeai::{types::*, Client}; #[tokio::main] async fn main() -> Result<(), plungeai::Error> { let client = Client::new()?; // reads PLUNGEAI_API_KEY; or Client::builder().api_key("ozk_…").build()? let me = client.whoami().await?; let run = client .agents() .execute("calculator-agent", &AgentExecuteParams { prompt: Some("6*7".into()), ..Default::default() }) .await?; let hits = client .discovery() .search(&DiscoverySearchParams { q: Some("web search".into()), kind: Some(DiscoverySearchParamsKind::Agents), ..Default::default() }) .await?; Ok(()) } ``` Resources are methods on the client (`client.agents()`, `client.workflows().versions()`); every operation takes its path parameters first, then a reference to its params struct (`plungeai::types::Params`, all `Default`). Params and responses are `serde` structs under `plungeai::types`; optional fields are `Option`; response enums carry an `Unknown` variant, so a new server value never breaks a client. ## Streaming responses ```rust use futures_util::StreamExt; let mut stream = client .workflows() .execute_inline_stream(&WorkflowExecuteInlineStreamParams { workflow: workflow.into(), ..Default::default() }) .await?; while let Some(event) = stream.next().await { let event = event?; if event.event.as_deref() == Some("token") { let token: serde_json::Value = event.json()?; print!("{}", token["delta"]); } } ``` `event.data` is the raw string of the server-sent event; `event.json::()` parses it. Dropping the stream cancels the request. Methods that accept `stream: true` on the API have a `…_stream` twin (`client.agents().execute_stream`, `client.chat().completions().create_stream`) returning the same `Stream`. ## Auto-pagination ```rust let mut items = client.workflows().list(&WorkflowListParams { kind: Some(WorkflowListParamsKind::Bot), ..Default::default() }).await?.into_stream(); while let Some(w) = items.next().await { println!("{:?}", w?); } let mut page = client.workflows().list(&WorkflowListParams { limit: Some(50), ..Default::default() }).await?; loop { for w in &page.data { println!("{w:?}"); } if !page.has_next_page() { break; } page = page.next_page().await?; } ``` Every `Page` carries `request_id` and `headers` from the response that produced it (also after `next_page()`); paged methods have a `list_raw` form for symmetry with the other `_raw` methods. ## Handling errors ```rust match client.agents().execute("calculator-agent", &AgentExecuteParams { prompt: Some("6*7".into()), ..Default::default() }).await { Ok(run) => println!("{run:?}"), Err(plungeai::Error::Api(err)) if err.status == 424 => println!("{:?}", err.connect.map(|c| c.link)), Err(plungeai::Error::Api(err)) => println!("{} {:?} {:?}", err.status, err.code, err.request_id), Err(err) => return Err(err), } ``` One enum, `plungeai::Error`: `Api(Box)` for a response with an error status (`status`, `code` — the envelope's `error.code`, `message`, `request_id` — the `x-request-id` header, and `connect` on 424 with `provider`, `method`, `link`), `Connection` for a request that never got a response, `Timeout`, and `Decode` for a body that does not match its type. Predicates `err.status()`, `err.is_not_found()`, `err.is_rate_limit()` and so on classify without matching. ## Retries Failed requests are retried 2 times by default with short exponential backoff (honouring `Retry-After`, as seconds or an HTTP-date): connection errors, timeouts, 408, 429 and ≥ 500. 409 and 424 are never retried — on the One API they are semantic answers (`approval_required`, `duplicate_execution_id`, a missing connection), and a retried execute would start a second run. Set the count on the builder or per call: ```rust let client = Client::builder().max_retries(0).build()?; client.with_options(RequestOptions::new().max_retries(5)).agents().execute("calculator-agent", ¶ms).await?; ``` `RequestOptions::new().idempotency_key(key)` is sent as `x-trace-id`, the API's idempotency key: it becomes the run's execution id, is re-sent on every retry of the call, and a replay answers 409 `duplicate_execution_id`. ## Timeouts Each attempt — connect, headers and the non-streaming body read — times out after 10 minutes by default and fails with `Error::Timeout` (retried as above); streaming bodies are not time-limited. Set it on the builder or per call: `Client::builder().timeout(Duration::from_secs(20))`, `RequestOptions::new().timeout(Duration::from_secs(5))`. ## Raw responses ```rust let response = client.agents().execute_raw("calculator-agent", ¶ms).await?; println!("{} {:?}", response.status, response.request_id); let run: AgentExecuteResponse = response.parse()?; ``` `client.with_options(RequestOptions)` (`header`, `query`, `timeout`, `max_retries`, `idempotency_key`) returns a client whose every call carries those options (a `query` value overrides a same-named params field, as in the TypeScript SDK), and every JSON operation has a `…_raw` form returning the response with its `status`, `headers`, `request_id` and `body` bytes. `client.get(path)`, `client.post(path, &body)` and friends reach routes the generated layer does not cover. ## Models plane `client.chat().completions().create(...)` (plus `client.embeddings()` and `client.models()`) covers the OpenAI-compatible routes here; any OpenAI client also works with base URL `https://api.plungeai.com/v1` and the same key. ## Requirements Rust 1.85 or newer (edition 2021); `tokio` runtime. TLS through rustls — no system OpenSSL needed. # PlungeAI in Claude Code Source: https://www.plungeai.com/docs/integrations/claude-code Planned: not available yet. Tracked as TI-74. ## What this will do One `claude mcp add` command, or a project `.mcp.json`. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in Claude.ai and Claude Desktop Source: https://www.plungeai.com/docs/integrations/claude-ai Planned: not available yet. Tracked as TI-74. ## What this will do Skill upload on claude.ai; mcp-remote bridge on Desktop. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in Codex Source: https://www.plungeai.com/docs/integrations/codex Planned: not available yet. Tracked as TI-74. ## What this will do `codex mcp add` with a bearer-token env var. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in Cursor Source: https://www.plungeai.com/docs/integrations/cursor Planned: not available yet. Tracked as TI-74. ## What this will do One-click deeplink or `~/.cursor/mcp.json`. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in VS Code Source: https://www.plungeai.com/docs/integrations/vscode Planned: not available yet. Tracked as TI-74. ## What this will do One-click install or `code --add-mcp`. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in Gemini CLI Source: https://www.plungeai.com/docs/integrations/gemini-cli Planned: not available yet. Tracked as TI-74. ## What this will do `~/.gemini/settings.json` with `httpUrl`. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in OpenCode Source: https://www.plungeai.com/docs/integrations/opencode Planned: not available yet. Tracked as TI-74. ## What this will do `opencode.json` remote MCP entry. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in Windsurf Source: https://www.plungeai.com/docs/integrations/windsurf Planned: not available yet. Tracked as TI-74. ## What this will do `~/.codeium/windsurf/mcp_config.json` snippet. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in Replit Source: https://www.plungeai.com/docs/integrations/replit Planned: not available yet. Tracked as TI-74. ## What this will do One-click install into Replit Agent. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in Lovable Source: https://www.plungeai.com/docs/integrations/lovable Planned: not available yet. Tracked as TI-74. ## What this will do Chat connector or app connector. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in Bolt.new Source: https://www.plungeai.com/docs/integrations/bolt Planned: not available yet. Tracked as TI-74. ## What this will do Custom MCP server in Connectors. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # PlungeAI in v0 Source: https://www.plungeai.com/docs/integrations/v0 Planned: not available yet. Tracked as TI-74. ## What this will do Add MCP in the prompt form; the generated app cannot call MCP. This page is not written yet; the source below covers the same material today. ## Use this today The agent skill that covers the same ground. # Any other MCP client Source: https://www.plungeai.com/docs/integrations/other-mcp-clients Planned: not available yet. Tracked as TI-74. ## What this will do URL, header and transport for clients not listed here. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Agentic payments (x402) Source: https://www.plungeai.com/docs/integrations/agentic-payments Planned: not available yet. Tracked as TI-74. ## What this will do Pay x402 services in USDC through the payment agents, with approval. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # AWS Marketplace Source: https://www.plungeai.com/docs/integrations/aws-marketplace Planned: not available yet. Tracked as TI-45. ## What this will do You will be able to subscribe to PlungeAI and pay through your AWS account. There is no AWS Marketplace listing today. ## Use this today Top up the wallet and see spend in Dashboard → One API → Billing. # Browser Use Source: https://www.plungeai.com/docs/integrations/browser-use Planned: not available yet. Tracked as TI-46. ## What this will do A browser-agent integration will let a workflow read pages that need a logged-in browser. No Browser Use integration exists today. ## Use this today The tool agents you can call today. # Google Cloud Marketplace Source: https://www.plungeai.com/docs/integrations/google-cloud-marketplace Planned: not available yet. Tracked as TI-45. ## What this will do You will be able to subscribe to PlungeAI and pay through your Google Cloud account. There is no Google Cloud Marketplace listing today. ## Use this today Top up the wallet and see spend in Dashboard → One API → Billing. # Google Gemini Enterprise Source: https://www.plungeai.com/docs/integrations/google-gemini-enterprise Planned: not available yet. Tracked as TI-46. ## What this will do Gemini Enterprise will be able to use PlungeAI agents and tools as a grounding source. No Gemini Enterprise integration exists today. ## Use this today Use PlungeAI from Gemini CLI over MCP today. # Google Sheets Source: https://www.plungeai.com/docs/integrations/google-sheets Planned: not available yet. Tracked as TI-46. ## What this will do A Google Sheets function will run a PlungeAI agent over a range of cells and write the answers back. No Sheets function exists today. ## Use this today Run a registry agent over the One API. # LiteLLM Source: https://www.plungeai.com/docs/integrations/litellm Planned: not available yet. Tracked as TI-74. ## What this will do Use the models plane as an OpenAI-wire LiteLLM provider. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # n8n Source: https://www.plungeai.com/docs/integrations/n8n Planned: not available yet. Tracked as TI-44. ## What this will do n8n nodes will run PlungeAI agents, tools and workflows inside an n8n flow. No n8n node exists today. An HTTP Request node that calls the One API is the nearest path; it is not tested end to end. ## Use this today Run a workflow over the One API. # OpenRouter compatibility Source: https://www.plungeai.com/docs/integrations/openrouter Planned: not available yet. Tracked as TI-74. ## What this will do Endpoint and field parity with OpenRouter, and what you gain. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Render Source: https://www.plungeai.com/docs/integrations/render Planned: not available yet. Tracked as TI-46. ## What this will do A Render template will deploy a sample app that calls PlungeAI in one click. No template exists today. ## Use this today Generate a typed client for your own app from the OpenAPI spec. # Superhuman Source: https://www.plungeai.com/docs/integrations/superhuman Planned: not available yet. Tracked as TI-46. ## What this will do Superhuman users will be able to call PlungeAI skills from their inbox. No Superhuman integration exists today. ## Use this today Connect any MCP-capable client to `https://mcp.plungeai.com/v1`. # Vercel Source: https://www.plungeai.com/docs/integrations/vercel Planned: not available yet. Tracked as TI-74. ## What this will do Vercel AI SDK against the models plane; v0 over MCP. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Zapier Source: https://www.plungeai.com/docs/integrations/zapier Planned: not available yet. Tracked as TI-44. ## What this will do A Zapier app will run PlungeAI agents and workflows as Zap actions. No Zapier app exists today. ## Use this today Run a workflow over the One API. # Data Integrations Source: https://www.plungeai.com/docs/data-integrations/overview Planned: not available yet. Tracked as TI-43. ## What this will do Data integrations will let Spark, DuckDB, BigQuery, Polars, Snowflake and Supabase run PlungeAI agents over the rows of a table or a dataframe. None exists today. The nearest path today is calling `POST /v1/agents/:id/execute` from a UDF or an edge function, or the models plane. ## Use this today Planned. Planned. Planned. Planned. Planned. Planned. Enrich a list of rows with a workflow you can run today. # Apache Spark Source: https://www.plungeai.com/docs/data-integrations/spark Planned: not available yet. Tracked as TI-43. ## What this will do The Apache Spark integration will provide SQL-native UDFs that call PlungeAI agents. It does not exist yet. The nearest path today is calling `POST /v1/agents/:id/execute` from a UDF or an edge function, or the models plane. ## Use this today Enrich a list of rows with a workflow you can run today. # DuckDB Source: https://www.plungeai.com/docs/data-integrations/duckdb Planned: not available yet. Tracked as TI-43. ## What this will do The DuckDB integration will provide batch enrichment from DuckDB queries. It does not exist yet. The nearest path today is calling `POST /v1/agents/:id/execute` from a UDF or an edge function, or the models plane. ## Use this today Enrich a list of rows with a workflow you can run today. # Google BigQuery Source: https://www.plungeai.com/docs/data-integrations/bigquery Planned: not available yet. Tracked as TI-43. ## What this will do The Google BigQuery integration will provide BigQuery remote functions that call PlungeAI agents. It does not exist yet. The nearest path today is calling `POST /v1/agents/:id/execute` from a UDF or an edge function, or the models plane. ## Use this today Enrich a list of rows with a workflow you can run today. # Polars Source: https://www.plungeai.com/docs/data-integrations/polars Planned: not available yet. Tracked as TI-43. ## What this will do The Polars integration will provide a Polars expression plugin that calls PlungeAI agents. It does not exist yet. The nearest path today is calling `POST /v1/agents/:id/execute` from a UDF or an edge function, or the models plane. ## Use this today Enrich a list of rows with a workflow you can run today. # Snowflake Source: https://www.plungeai.com/docs/data-integrations/snowflake Planned: not available yet. Tracked as TI-43. ## What this will do The Snowflake integration will provide a Snowflake UDTF that calls PlungeAI agents. It does not exist yet. The nearest path today is calling `POST /v1/agents/:id/execute` from a UDF or an edge function, or the models plane. ## Use this today Enrich a list of rows with a workflow you can run today. # Supabase Source: https://www.plungeai.com/docs/data-integrations/supabase Planned: not available yet. Tracked as TI-43. ## What this will do The Supabase integration will provide Supabase Edge Function helpers that call PlungeAI agents. It does not exist yet. The nearest path today is calling `POST /v1/agents/:id/execute` from a UDF or an edge function, or the models plane. ## Use this today Enrich a list of rows with a workflow you can run today. # PlungeAI API Glossary Source: https://www.plungeai.com/docs/resources/glossary Planned: not available yet. Tracked as TI-74. ## What this will do Every term used across the API, MCP and skills docs. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Connector catalog Source: https://www.plungeai.com/docs/resources/connector-catalog Planned: not available yet. Tracked as TI-74. ## What this will do OAuth and API-key connectors, BYOK, and how connected accounts reach calls. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Data and privacy Source: https://www.plungeai.com/docs/resources/data-and-privacy This page covers the models plane: `/v1/chat/completions`, `/v1/completions` and `/v1/embeddings` on the inference gateway. ## What a request row holds Every models-plane request writes one row to the `inference_requests` table and one data point to Cloudflare Workers Analytics Engine. The row holds metadata only: | Column | What it is | |---|---| | `id` | The server-generated request id | | `api_key_id`, `org_id` | The key that made the call and its organisation | | `provider`, `model` | The provider and model that served the request | | `model_requested` | The model or first candidate you asked for | | `route_attempts`, `route_sort` | The routing trail: for each attempt the slug, provider, HTTP status, duration and reason; and the `sort` you sent | | `stream` | Whether the response was streamed | | `prompt_tokens`, `completion_tokens`, `total_tokens` | Token counts | | `cache_read_tokens`, `cache_write_tokens`, `cache_savings_usd` | The provider prompt-cache share of the prompt tokens and what it saved | | `cost_usd`, `price_usd`, `margin_usd` | Upstream cost, the price charged, and the difference | | `ttft_ms`, `latency_ms` | Time to first token and total latency | | `status`, `error_code` | HTTP status and error code | | `byok` | Whether your own provider key was used | | `client_request_id` | The `x-request-id` you sent, if any | | `created_at` | When the request arrived | No prompt text and no completion text is stored in the row. The Analytics Engine data point carries the organisation, provider, model, error code, BYOK flag, token counts, cost, latencies and status, and no text either. ## Response cache The exact-match response cache is off by default. An organisation opts in, and only a non-streamed request with an explicit `temperature: 0` is eligible. When it is on, the full response body is stored in Cloudflare KV under a SHA-256 hash of your organisation id and the request body. The cache lifetime is the organisation's setting (default 300 seconds), clamped to between 60 seconds and 24 hours. A hit never crosses an organisation boundary. ## Retention - Request rows (`inference_requests`) have no scheduled deletion today: they are kept for billing and reconciliation until the account is erased. An account erasure request deletes the organisation's rows. - Cached responses expire with the cache lifetime above, 24 hours at most. - Sticky-routing pins (a hash of the conversation prefix and the provider that served it, never the text) expire after 300 seconds. ## Sub-processors Your prompt and the completion pass through the upstream provider that serves the request. The gateway can route to the providers below; when you bring your own key (BYOK), your own agreement with that provider applies. The gateway runs in Cloudflare's San Jose region; each provider processes the request in its own infrastructure, under its own terms. | Provider | Company | Status | Data-use policy | Trains on API traffic? | |---|---|---|---|---| | `openai` | OpenAI | Serving | [OpenAI enterprise privacy](https://openai.com/enterprise-privacy/) | No, by default (API data is not used for training unless the customer opts in) | | `anthropic` | Anthropic | Serving | [Anthropic commercial terms](https://www.anthropic.com/legal/commercial-terms) | No (commercial terms) | | `google` | Google (Gemini API) | Serving | [Gemini API terms](https://ai.google.dev/gemini-api/terms) | See vendor policy (depends on the billing tier) | | `groq` | Groq | Serving | [Groq privacy policy](https://groq.com/privacy-policy/) | See vendor policy | | `mistral` | Mistral AI | Serving | [Mistral AI legal terms](https://mistral.ai/terms) | See vendor policy | | `deepseek` | DeepSeek | Serving | [DeepSeek](https://www.deepseek.com/) | See vendor policy | | `xai` | xAI | Serving | [xAI privacy policy](https://x.ai/legal/privacy-policy) | See vendor policy | | `together` | Together AI | Serving | [Together AI privacy policy](https://www.together.ai/privacy) | See vendor policy | | `fireworks` | Fireworks AI | Serving | [Fireworks AI privacy policy](https://fireworks.ai/privacy-policy) | See vendor policy | | `cerebras` | Cerebras | Serving | [Cerebras privacy policy](https://www.cerebras.ai/privacy-policy) | See vendor policy | | `perplexity` | Perplexity | Listed in the router, currently not serving (account credits) | [Perplexity privacy policy](https://www.perplexity.ai/hub/legal/privacy-policy) | See vendor policy | | `cohere` | Cohere | Serving | [Cohere privacy policy](https://cohere.com/privacy) | See vendor policy | | `openrouter` | OpenRouter | Serving | [OpenRouter privacy policy](https://openrouter.ai/privacy) | See vendor policy (OpenRouter forwards to further providers) | | `moonshot` | Moonshot AI | Serving | [Moonshot AI platform](https://platform.moonshot.cn/) | See vendor policy (endpoint `api.moonshot.cn`) | | `deepinfra` | DeepInfra | Serving | [DeepInfra privacy policy](https://deepinfra.com/privacy) | See vendor policy | | `novita` | Novita AI | Listed in the router, currently not serving (no platform key) | [Novita AI privacy policy](https://novita.ai/legal/privacy-policy) | See vendor policy | | `nebius` | Nebius | Listed in the router, currently not serving (no platform key) | [Nebius privacy policy](https://nebius.com/privacy-policy) | See vendor policy | Cloudflare hosts the gateway, its D1 databases, KV and Analytics Engine. ## Data processing agreement A data processing agreement (DPA) is available on request from PlungeAI support. ## SOC 2 PlungeAI is not yet SOC 2 certified, and no SOC 2 audit has been completed. The gateway writes a nightly, hash-chained reconciliation record of the billing ledger against the request rows, kept as evidence for a future audit. ## Region The gateway and every other PlungeAI Worker run placed in one region: San Jose (Cloudflare colo SJC). Upstream providers process requests in their own regions. # Memory Source: https://www.plungeai.com/docs/resources/memory Planned: not available yet. Tracked as TI-74. ## What this will do Long-term memory, and distilling sessions into skills. This page is not written yet; the source below covers the same material today. ## Use this today The section of the MCP developer guide this page will restate. # Roles and permissions Source: https://www.plungeai.com/docs/resources/roles-and-permissions Planned: not available yet. Tracked as TI-74. ## What this will do Company and team roles, scopes and levels in the Dashboard. This page is not written yet; the source below covers the same material today. ## Use this today Teams, companies and roles live in Dashboard → Team and Dashboard → Workspace. # Trust fence and approvals Source: https://www.plungeai.com/docs/resources/trust-fence Planned: not available yet. Tracked as TI-74. ## What this will do Key fences, the trust fence, the agent fence, mission bounds and model guardrails. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # API Error Codes and Warnings Source: https://www.plungeai.com/docs/resources/warnings-and-errors Planned: not available yet. Tracked as TI-74. ## What this will do Every error envelope and code, the outcomes, and what to retry. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Webhook setup Source: https://www.plungeai.com/docs/resources/webhook-setup Planned: not available yet. Tracked as TI-25. ## What this will do PlungeAI will call a URL you register when a workflow run finishes, signed so you can verify the sender, with retries on failure. Nothing sends outbound webhooks today. ## Use this today Follow a run live over SSE, or start it with sync:false and poll the results route. # Migrate from legacy execute aliases Source: https://www.plungeai.com/docs/workflows/migration-guide Planned: not available yet. Tracked as TI-74. ## What this will do Move `/v1/execute` and `/v1/cnl/*` calls to `/v1/workflows/*`. This page is not written yet; the source below covers the same material today. ## Use this today The section of the One API developer guide this page will restate. # Docs for AI agents Source: https://www.plungeai.com/docs/resources/docs-for-agents Partly available: the Docs MCP server, which will let your agent search and read these pages. Tracked as TI-35. Every page under www.plungeai.com/docs has a markdown form, the whole site has an index, and one file tells an agent how to connect PlungeAI. ## /agents.md [https://www.plungeai.com/docs/agents.md](/agents.md) is the setup file for coding agents: get an `ozk_` key from the user, connect the client to `https://mcp.plungeai.com/v1`, verify with `plungeai_whoami`, add the skills and follow the rules. Paste this prompt into any agent to run it: ```text Use curl to read https://www.plungeai.com/docs/agents.md and perform the setup to connect PlungeAI ``` It is served as `text/markdown; charset=utf-8`, and it is not a sidebar page. ## llms.txt and llms-full.txt | File | What it holds | |---|---| | [/llms.txt](/llms.txt) | `# PlungeAI`, a one-line description, then one bullet per page in sidebar order (`[Title](https://www.plungeai.com/docs/.md): description`), with ` (planned)` after pages that are not available yet; then `## OpenAPI Specs` and `## Optional` | | [/llms-full.txt](/llms-full.txt) | Every page in full: `# `, a `Source:` line with the page URL, then the page body; API and MCP reference pages in a condensed form | ## Markdown twins Append `.md` to any page URL, for example [/getting-started/quickstart.md](/getting-started/quickstart.md), or request the page with `Accept: text/markdown`: ```bash curl -s -H 'Accept: text/markdown' https://www.plungeai.com/docs/getting-started/quickstart ``` A twin starts with a pointer to `/llms.txt`, then the page title and description, then the page source unchanged. Twins carry `X-Robots-Tag: noindex`, so search engines index the HTML page only. ## Docs MCP A docs MCP server at `www.plungeai.com/docs/mcp` will let an agent search these docs and read a page by path. It is planned (TI-35) and answers with a JSON-RPC error until then. The product MCP server at `https://mcp.plungeai.com/v1` is a different server and is live. <Card title="Docs MCP server" icon="plug" href="/developer-tools/mcp/docs-mcp"> What the docs MCP server will do. </Card> ## Other llms.txt files | URL | Covers | |---|---| | https://api.plungeai.com/llms.txt | The One API guide | | https://mcp.plungeai.com/llms.txt | Workflow authoring over MCP (the CNL skill) | | https://skills.plungeai.com/llms.txt | The agent skills catalog | # Status Source: https://www.plungeai.com/docs/resources/status ## Status page The inference gateway publishes a public status page at [https://gateway.plungeai.com/status](https://gateway.plungeai.com/status). It needs no API key. It shows one row per upstream provider the gateway can route to: | Column | What it is | |---|---| | State | `operational`, `degraded` (under 95% of the last 24 hours' requests to that provider succeeded) or `outage` (the gateway has the provider marked out after repeated upstream failures and is routing around it) | | Success (24 h) | The share of the last 24 hours' requests to that provider that did not end in a 5xx error | | p50 TTFT ms (1 h) | The median time to first token over the last hour's requests that recorded one, in milliseconds | A provider with no traffic in the window shows a dash. The same data is available as JSON at `https://gateway.plungeai.com/status.json`: ```json { "generated_at": "2026-10-14T18:00:00.000Z", "providers": [ { "provider": "openai", "state": "operational", "success_24h": 0.9998, "p50_ttft_ms_1h": 412 } ] } ``` Both are aggregates across all customers. They never carry an organisation, key or request. The snapshot is recomputed at most once a minute. ## Readiness and liveness | Endpoint | Answers | |---|---| | `GET https://gateway.plungeai.com/health` | Readiness. `200` when the gateway can serve, `503` when it cannot | | `GET https://gateway.plungeai.com/healthz` | Liveness. Always `200` with a fixed body while the Worker runs; it touches nothing | `/health` runs three checks at once and reports each one: | Check | What it tests | On failure | |---|---|---| | `billing_db` | A query against the billing database that holds balances and request rows | `status: "down"`, HTTP 503 | | `kv` | A read from the key-value store that holds rate-limit counters and routing state | `status: "down"`, HTTP 503 | | `ratecard` | The age, in seconds, of the published rate card the gateway prices requests from | Older than 26 hours, or no publish time: `status: "degraded"`, HTTP 200 | The body also lists `providers_out`: the providers currently marked out after upstream failures. A marked provider does not make the gateway unready; it shows `outage` on the status page. ```json { "status": "ok", "service": "inference-gateway", "env": "production", "checks": { "billing_db": { "ok": true, "ms": 4 }, "kv": { "ok": true, "ms": 2 }, "ratecard": { "ok": true, "age_s": 3600 } }, "providers_out": [] } ``` ## Other health endpoints | Service | URL | |---|---| | One API | https://api.plungeai.com/health | | MCP server | https://mcp.plungeai.com/health and https://mcp.plungeai.com/v1/info | | Skills site | https://skills.plungeai.com/health | | Landing site | https://plungeai.com/health | ## Reporting a problem If the status page shows every provider operational and your requests still fail, contact PlungeAI support with the `x-request-id` response header of a failing request. # FAQs Source: https://www.plungeai.com/docs/resources/faqs <Info> Planned: not available yet. Tracked as TI-74. </Info> ## What this will do Answers to common platform, API, billing and security questions. This page is not written yet; the source below covers the same material today. ## Use this today <Card title="MCP guide: 15.1 Five first checks" icon="book" href="/mcp/troubleshooting#151-five-first-checks"> The section of the MCP developer guide this page will restate. </Card> <!-- placeholder-source: content/_placeholders.json --> # One API guide Source: https://www.plungeai.com/docs/one-api The **Ocean One API** is a single HTTPS front door to the Ocean platform. One base URL, one router, and every capability the platform exposes reachable from it: https://api.plungeai.com Everything in this guide is a real route on that host. The router is a thin Hono worker (`orchestration/api-gateway/api-gateway.ts`) that fronts the registry, the CNL engine, the MCP gateway, the MCP executor, and the inference gateway over internal service bindings — you never address those services directly. ## Start here — one call, 30 seconds Get an `ozk_` key at Dashboard → **One API → Keys** (`https://dashboard.plungeai.com`), then: ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"In one sentence: what is a content delivery network?","sync":true,"max_tokens":60}' ``` Add `"stream": true` and you get OpenAI-shaped chunks; send the same fields as YAML with `format: markdown` and you get Markdown back. Thirty-five more pasteable calls — streaming and not, JSON and YAML, tools, workflows, MCP, create-and-run — are in the [Quickstart](/one-api/quickstart). ## Three levels The One API has three levels. All three stay: none is deprecated in favour of another, and they share the one `ozk_` key, the request ids and the error envelope. | Level | What it is | Use it when | Details | |---|---|---|---| | **1. The compatible wire** | OpenAI-shaped `POST /v1/chat/completions`, `/v1/embeddings`, `/v1/completions`, `GET /v1/models`, and the OpenRouter drop-in base `/api/v1`. Models only | you already have an OpenAI or OpenRouter client: swap the base URL | [Models plane](/one-api/models), [OpenRouter compatibility](/one-api/openrouter-compat) | | **2. Standard REST** | resources with JSON bodies; YAML is accepted and returned on the same routes. Workflows (create, version, run), executions, schedules, triggers, bot policy and budgets, workspace, inbox, bindings, groups, memory, skills, templates, connections, tools, agents, discovery, traces | you manage things or call one by id: create a bot, cap its spend, answer a paused run, list schedules | the plane chapters, and [Bots](/one-api/bots) for the bot surface | | **3. The Agent Call door** | `POST /v1/execute`: the body is the program. A bare task, a `workflow:`, or a saved agent `agent: <id>[@n]`; always through the engine; `dry_run`, `stream`, and headers that say what ran | the body is the program: one call, no resource to create first | [The one door](/one-api/bots#169-the-one-door) | **Start with level 2; use level 3 when the body is the program.** Level 2 gives every resource its own route, status codes and ids, so it is the one to build on. Level 3 is one route that takes whatever you send and runs it; it is also how an agent calls another agent by id. The same machine-readable contract covers all three: `GET /v1/openapi.json` tags the operations `Compat` (level 1), `Workflows`, `Executions`, `Schedules`, `Triggers`, `Bots`, `Memory`, `Skills`, `Templates`, `Connections` (level 2) and `Agent Call` (level 3). The older level 2 planes (discovery, tools, agents, MCP, traces) and the meta routes carry no tag. A level 2 call, read-only and live-verified: ```bash curl -s "https://api.plungeai.com/v1/workflows?limit=1" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ## The seven planes The router groups the routes of levels 1 and 2 into planes. Every plane takes the same `ozk_` platform key; six are the **execution planes**, and the **models plane** is OpenAI-compatible. The resources that manage bots (saved workflows, executions, schedules, triggers, permissions, memory, skills, templates, connections) are level 2 as well and are in [Bots](/one-api/bots). | Plane | What it does | Key routes | Key | |---|---|---|---| | **Models** | OpenAI-compatible chat, embeddings, model catalog — a transparent proxy to the inference gateway with ordered fallback, `sort`, `@preset/<slug>`, guardrails, and an opt-in response cache | `POST /v1/chat/completions` · `POST /v1/messages` (Anthropic wire, [3.16](/one-api/models#316-anthropic-messages-post-v1messages)) · `POST /v1/embeddings` · `GET /v1/models` · the same routes under `/api/v1` (OpenRouter drop-in, [5.0](/one-api/openrouter-compat#50-drop-in-mode)) | `ozk_` | | **Tools** | List tool agents, read a tool's invocation contract, execute one operation behind a trust fence | `GET /v1/tools` · `GET /v1/tools/{id}` · `POST /v1/tools/{id}/execute` | `ozk_` | | **Agents** | Execute a registry agent from a prompt — sync, async pointer, or OpenAI-shaped SSE | `GET /v1/agents` · `POST /v1/agents/{id}/execute` · `GET /v1/agents/results/{workflowId}/{taskId}` | `ozk_` | | **Workflows** | Run a multi-agent CNL workflow, inline or saved, with optional SSE | `POST /v1/workflows/execute` · `POST /v1/workflows/execute-stream` · `POST /v1/workflows/{id}/execute` | `ozk_` | | **MCP** | The platform *as* an MCP server (inbound Streamable HTTP), plus outbound MCP runs against catalog servers — full guide: [MCP developer guide](https://www.plungeai.com/docs/mcp) | `POST /v1/mcp` · `GET /v1/mcp/tools` · `POST /v1/mcp/runs` | `ozk_` | | **Discovery** | Search and recommend across the capability registry (agents, skills, connectors, models, MCP servers, …) | `GET /v1/discovery/search` · `POST /v1/discovery/recommend` · `GET /v1/discovery/cards/{type}/{id}` | `ozk_` | | **Traces** | Read the persisted execution trace (spans + gateway request log) for one trace id | `GET /v1/traces/{id}` | `ozk_` | The full machine-readable route list and schemas are served at `GET /v1/openapi.json` (source: `orchestration/api-gateway/openapi.ts`). There is also a signed, internal-only capability plane that customer keys cannot open — it is not one of the seven and is not documented here. ## Positioning Think of the One API as **an OpenRouter-compatible models plane plus everything a model router cannot do.** The models plane speaks the OpenAI wire format, so an existing OpenAI or OpenRouter integration points at it with a base-URL swap (see [OpenRouter compatibility](/one-api/openrouter-compat)). The execution planes are the part a router has no answer for: real tools with contracts and a trust fence, registry agents, multi-agent workflows, discovery, MCP, and a persisted trace for every run. ## One key | Prefix | Opens | How you get it | |---|---|---| | `ozk_` | Every plane: models, tools, agents, workflows, MCP, discovery, traces, and the connector proxy | Your **Ocean account key**, at Dashboard → **One API → Keys** (`https://dashboard.plungeai.com`), or fetched by `ocean-cli login` / `oceancode login --provider ocean` after a browser sign-in; bound to your account and tier, and billed to the account's one wallet | Everything is on `https://api.plungeai.com` (MCP also on `https://mcp.plungeai.com`). The legacy `sk-ocean-` (models) and `sk-conn-` (connector proxy) keys are still accepted on their own plane while integrations move over, no longer created in the Dashboard. A legacy key sent to an execution plane is a `401` — see [Authentication & keys](/one-api/authentication). Full key mechanics are in that chapter; this guide writes the key as the shell variable `$PLUNGE_API_KEY` (`ozk_`). ## How a request flows An execution-plane call is not a thin proxy to a model. The path is **router → CNL engine → agent**: 1. The router authenticates the `ozk_` key, mints a server-side `x-request-id`, and either adopts your UUID `x-trace-id` or generates the trace id (`api-gateway.ts`). 2. It dispatches over an internal service binding to the CNL engine, which loads the agent card from the registry, refuses anything not `status:active` before dispatch, recalls memory, and runs the agent. 3. The agent's result is stored in SharedMemory and returned; the whole run is persisted as trace spans. The engine hop exists on purpose: it gives every call the same history, outcome vocabulary (`ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error`), and trust fence, whether the caller is a workflow, Ocean Studio, or you over HTTP. The rationale is recorded in `orchestration/api-gateway/log/API-SURFACE-DECISION-AND-PLAN.md` (everything routes through the engine so there is one execution path, not two). ## What "live-verified" means in this guide Every example that carries a hidden `# verify` / `# expect:` marker was run against the deployed API and its output checked. The docs build strips the marker; a verifier (`orchestration/api-gateway/docs/guide-3.0/verify-examples.mjs`) re-runs those examples with one `ozk_` key. A markered chat example sends one small request (`max_tokens` ≤ 16); examples that spend more, need a connected account or change state carry no marker and say so. ## 60-second tour Set your key once, then hit three planes. These three are live-verified: ```bash curl -s https://api.plungeai.com/health ``` ```bash curl -s "https://api.plungeai.com/v1/discovery/search?q=web+search&kind=agents&limit=3" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ```bash curl -s "https://api.plungeai.com/v1/agents?limit=3" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` For a first tool call, agent call, workflow, and model completion, go straight to the [Quickstart](/one-api/quickstart). ## What's new in 3.1 - **Three levels** — the compatible wire, standard REST in JSON or YAML, and the Agent Call door `POST /v1/execute`; start with level 2 ([Three levels](#three-levels)). - **The bot surface over REST** — a bot is a saved `kind: bot` workflow that runs on the `one-agent` runtime. Save and version it, set its permissions and budgets, answer and approve its runs, schedule and trigger it, read its workspace, inbox, bindings and groups, and use memory, skills and templates ([Bots](/one-api/bots)). The same operations are MCP actions ([MCP developer guide](https://www.plungeai.com/docs/mcp)) and `ocean-cli` verbs. ## What's new in 3.0 - **Full field-level models reference** — every `/v1/chat/completions` field with its support status ([Models plane](/one-api/models)). - **OpenRouter compatibility matrix** — endpoint and field parity, same / differs / missing / Ocean-only ([OpenRouter compatibility](/one-api/openrouter-compat)). - **Connectors chapter** — OAuth vs API-key connectors, connected accounts, the connector proxy ([Connectors](/one-api/connectors)). - **Agent tiers** — tool agents, `one-agent` (the runtime: call mode and loop mode), `concierge-agent`, `claude-managed-agent` ([Agents plane](/one-api/agents)). - **Capability injection** — skills, experts, persona, backgrounds, plugins, and MCP on harness missions ([Skills, plugins & capability injection](/one-api/skills-plugins-capabilities)). - **SDK matrix** — OpenAI SDKs, `@plungeai/one-api`, LiteLLM, Vercel AI SDK, LangChain, and MCP clients ([SDKs & clients](/one-api/sdks-and-clients)). - **Website copy** — this guide is also published at `https://plungeai.com/docs`. ## Where to get help - **Docs:** `https://www.plungeai.com/docs/one-api` (this guide) · `GET /llms.txt` and `GET /llms-full.txt` for LLM tooling · `GET /v1/openapi.json` for the contract. - **Keys and support:** [Requests](https://www.plungeai.com/request/new?kind=question). - **Every response** carries an `x-request-id` — include it when you report a problem ([Traces & observability](/one-api/traces-observability)). # Quickstart Source: https://www.plungeai.com/docs/one-api/quickstart Thirty-five calls you can paste into a terminal and run as they are. Every `ozk_` sample below is live-verified on the build date (the verifier runs the ones marked in the source; the create/run/delete ones mutate state and are run by hand). Two minutes of setup: 1. Get an `ozk_` key: Dashboard → **One API → Keys** (`https://dashboard.plungeai.com`), copy it once. 2. Export it, and paste the four helpers once per terminal. `live` prints a stream's text as it arrives (OpenAI-shaped chunks or engine token frames), `frames` prints the raw SSE, `mcp` sends one MCP call, `mcptext` prints its rendered answer. ```bash # prelude export PLUNGE_API_KEY=ozk_… # paste your key here once per terminal live() { python3 -u -c ' import sys, json for line in iter(sys.stdin.readline, ""): if not line.startswith("data:"): continue b = line[5:].strip() if b == "[DONE]": break try: d = json.loads(b) except Exception: continue t = ((d.get("choices") or [{}])[0].get("delta") or {}).get("content") if "choices" in d else d.get("delta") if t: sys.stdout.write(t); sys.stdout.flush() print()'; } frames() { grep --line-buffered -E '^(event|data|id):'; } mcp() { curl -sN https://api.plungeai.com/v1/mcp -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$1"; } mcptext() { python3 -c 'import sys,json;raw=sys.stdin.read();fr=[l[5:].strip() for l in raw.splitlines() if l.startswith("data:")];d=json.loads(fr[-1] if fr else raw);r=d.get("result") or d;print(r["content"][0]["text"] if isinstance(r,dict) and r.get("content") else json.dumps(r,indent=1))'; } uuid() { grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1; } ``` The wires are independent: send JSON or YAML, get `json`, `yaml`, `markdown` or `text` back (`format` in the body, `?format=` on the URL, or an `Accept` header — see [Workflows plane § response formats](/one-api/workflows)). Streams have one fixed shape per plane. The same `$PLUNGE_API_KEY` opens every plane, the models plane (OpenAI SDK) included, which is at the end of this chapter. ## 1.1 Your first three calls An agent, synchronous, JSON in → JSON out: ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"In one sentence: what is a content delivery network?","sync":true,"max_tokens":60}' ``` The same agent, **streamed** — OpenAI-shaped chunks, ends with `data: [DONE]`: ```bash curl -sN -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"Write two markdown bullets about glaciers.","provider":"openai","model":"gpt-4o-mini","max_tokens":100,"stream":true}' ``` The same agent, **YAML in → Markdown out**: ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' prompt: "In two markdown bullets, what does a content delivery network do? Bold the key term in each." provider: openai model: gpt-4o-mini maxTokens: 150 sync: true format: markdown EOF ``` The same first call in Python and TypeScript (plain HTTP, no SDK): ```python import requests r = requests.post( "https://api.plungeai.com/v1/agents/one-agent/execute", headers={"Authorization": "Bearer <PLUNGE_API_KEY>"}, json={"prompt": "In one sentence: what is a content delivery network?", "sync": True, "max_tokens": 60}, ) print(r.json()["content"]) ``` ```ts const r = await fetch('https://api.plungeai.com/v1/agents/one-agent/execute', { method: 'POST', headers: { Authorization: 'Bearer <PLUNGE_API_KEY>', 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'In one sentence: what is a content delivery network?', sync: true, max_tokens: 60 }), }) console.log((await r.json()).content) ``` ## 1.2 YAML in → Markdown out (10) **A1** — direct agent call, `sync: true` + `format: markdown`. The body is the Markdown itself, then `---` and an `execution:` footer with the run id. ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' prompt: "Reply with exactly this markdown line: **yaml-md-ok**" provider: openai model: gpt-4o-mini maxTokens: 24 sync: true format: markdown EOF ``` **A2** — `format: text`: the bare content, no footer. ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' prompt: "Reply with exactly this markdown line: **yaml-md-ok**" provider: anthropic model: claude-sonnet-4-5-20250929 maxTokens: 24 sync: true format: text EOF ``` **A3** — no `format:` in the body; ask with the `Accept` header. Precedence: body `format` → `?format=` → `Accept` → mirror of the request Content-Type (YAML in, YAML out). ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' -H 'Accept: text/markdown' --data-binary @- <<'EOF' prompt: "Reply with exactly this markdown line: **yaml-md-ok**" provider: gemini maxTokens: 24 sync: true EOF ``` **A4** — `stream: true`, Markdown streamed live. `format:` never applies to a stream; the Markdown is what the model writes and `live` joins the `delta.content` pieces (what Studio and the CLI do). ```bash curl -sN -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' | live prompt: "Reply with exactly: stream-ok" provider: openai model: gpt-4o-mini maxTokens: 24 stream: true EOF ``` **A5** — the same stream as raw frames. ```bash curl -sN -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' | frames prompt: "Name two lakes in one markdown bullet each." provider: openai model: gpt-4o-mini maxTokens: 80 stream: true EOF ``` **A6** — a tool, `operation:` + its fields bare at the root → a Markdown document (Brave news). (`params:` still wraps them if you prefer.) ```bash curl -s -X POST https://api.plungeai.com/v1/tools/brave-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' operation: news query: "Cloudflare Workers" freshness: pw count: 3 format: markdown EOF ``` **A7** — a tool → Markdown (weather forecast). Requires nothing beyond the key. ```bash curl -s -X POST https://api.plungeai.com/v1/tools/weather-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' operation: forecast params: lat: 43.697 lon: -114.352 units: imperial cnt: 8 format: markdown EOF ``` **A8** — a whole workflow document with `format: markdown` and `input:` → the **result** as Markdown. `format:`, `input:` and `inputs:` next to `workflow:` are the envelope fields (the same ones Studio sends); `{input}` is substituted. Without `format:` you get the YAML pointer envelope (ids only). ```bash curl -s -X POST https://api.plungeai.com/v1/workflows/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' format: markdown input: "wf-md-ok" workflow: name: md-inline tasks: - type: task id: t1 agent: one-agent maxTokens: 40 prompt: "Reply with exactly this markdown line: **{input}**" EOF ``` **A9** — the same workflow on the stream. The gateway follows the provider's `stream_url` for you and injects the deltas as `event: token` frames — one connection. Pipe through `frames` to see the named engine events around them. ```bash curl -sN -X POST https://api.plungeai.com/v1/workflows/execute-stream \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' | live input: "wf-md-ok" workflow: name: md-stream tasks: - type: task id: t1 agent: one-agent provider: openai model: gpt-4o-mini maxTokens: 40 prompt: "Reply with exactly this markdown line: **{input}**" EOF ``` **A10** — MCP: the YAML travels inside the JSON-RPC frame; the answer is Markdown by default. ```bash mcp '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_workflow","arguments":{"user_request":"yaml in, markdown out","mode":"sync","input":"wf-md-ok","workflow_yaml":"workflow:\n name: md-mcp\n tasks:\n - type: task\n id: t1\n agent: one-agent\n maxTokens: 40\n prompt: \"Reply with exactly this markdown line: **{input}**\""}}}' | mcptext ``` ## 1.3 JSON in → JSON out (10) **B1** — agent, sync → JSON envelope (`content`, `workflow_id`, `task_id`, `request_id`). ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"In one sentence: what is a Cloudflare Worker?","sync":true,"max_tokens":60}' | python3 -m json.tool ``` **B2** — the generation knobs: `provider`, `model`, `temperature`, `max_tokens`. The Markdown sits inside the JSON `content` string. ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"List three facts about the moon as a markdown list.","provider":"openai","model":"gpt-4o-mini","temperature":0,"max_tokens":120,"sync":true}' | python3 -m json.tool ``` **B3** — `stream: true` → OpenAI-shaped chunk stream, raw. An OpenAI SDK streaming client works against this URL. ```bash curl -sN -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"Write two markdown bullets about glaciers.","provider":"openai","model":"gpt-4o-mini","max_tokens":100,"stream":true}' | frames ``` **B4** — the same stream, text only, live. ```bash curl -sN -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"Reply with exactly: json-stream-ok","provider":"openai","model":"gpt-4o-mini","max_tokens":24,"stream":true}' | live ``` **B5** — a tool, JSON body (`operation` + `params`) → JSON envelope. ```bash curl -s -X POST https://api.plungeai.com/v1/tools/exa-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"operation":"search","params":{"query":"latest developments in quantum computing","numResults":3,"searchType":"auto"}}' | python3 -m json.tool | head -40 ``` **B6** — a tool's invocation contract (operations, params, gates, examples). ```bash curl -s https://api.plungeai.com/v1/tools/brave-agent -H "Authorization: Bearer $PLUNGE_API_KEY" | python3 -m json.tool | head -60 ``` **B7** — a workflow as a JSON object, sync → JSON pointer, then the result as JSON. ```bash R=$(curl -s -X POST https://api.plungeai.com/v1/workflows/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"workflow":{"name":"json-wf","tasks":[{"type":"task","id":"t1","agent":"one-agent","maxTokens":60,"prompt":"Explain {input} in one sentence."}]},"input":"edge computing"}') echo "$R" | python3 -m json.tool WF=$(echo "$R" | python3 -c 'import sys,json;print(json.load(sys.stdin)["workflow_id"])') curl -s "https://api.plungeai.com/v1/workflows/results/$WF/t1" -H "Authorization: Bearer $PLUNGE_API_KEY" | python3 -m json.tool ``` **B8** — a workflow JSON body on the stream → the engine's named SSE events, every `data:` is JSON: `request_received → workflow_loaded → workflow_started → task_dispatched → streaming_started → token… → task_completed → workflow_completed → workflow_result`. ```bash curl -sN -X POST https://api.plungeai.com/v1/workflows/execute-stream \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"workflow":{"name":"json-stream","tasks":[{"type":"task","id":"t1","agent":"one-agent","provider":"openai","model":"gpt-4o-mini","maxTokens":80,"prompt":"Explain {input} in two markdown bullets."}]},"input":"edge computing"}' | frames ``` **B9** — discovery: hybrid search over the capability registry → JSON cards. ```bash curl -s "https://api.plungeai.com/v1/discovery/search?q=web+search&kind=agents&limit=3" \ -H "Authorization: Bearer $PLUNGE_API_KEY" | python3 -m json.tool | head -40 ``` **B10** — MCP with `format: "json"`: the tool text is a JSON document and `structuredContent` carries the same object. MCP's `format` enum is `markdown | json`; the JSON-RPC exchange arrives as SSE frames (progress first, the result last) — MCP does not token-stream. ```bash mcp '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"plungeai_execute_agent","arguments":{"user_request":"json please","agent":"one-agent","prompt":"Reply with exactly: mcp-json-ok","format":"json"}}}' | sed -n 's/^data: //p' | tail -1 | python3 -m json.tool ``` ## 1.4 Mixed wires (2) **M1** — JSON in → Markdown out. ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"Two markdown bullets about tides.","provider":"openai","model":"gpt-4o-mini","max_tokens":100,"sync":true,"format":"markdown"}' ``` **M2** — YAML in → JSON out. ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' | python3 -m json.tool prompt: "One sentence about tides." provider: openai model: gpt-4o-mini maxTokens: 60 sync: true format: json EOF ``` ## 1.5 Run, then create and run — always streaming (10) These create rows in your account (the same D1 rows Studio writes) and delete them at the end. Run them top to bottom in one terminal: the ids flow from one step to the next. **C1** — run an agent, streamed (Anthropic this time). ```bash curl -sN -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' | live prompt: "Reply with exactly: run-ok" provider: anthropic model: claude-sonnet-4-5-20250929 maxTokens: 24 stream: true EOF ``` **C2** — CREATE a workflow (`plungeai_workflow create`, `kind: workflow`). ```bash STAMP=$(date -u +%H%M%S) R=$(mcp "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"plungeai_workflow\",\"arguments\":{\"user_request\":\"create\",\"action\":\"create\",\"kind\":\"workflow\",\"name\":\"Quickstart workflow $STAMP\",\"yaml\":\"workflow:\\n name: quickstart-workflow\\n tasks:\\n - type: task\\n id: t1\\n agent: one-agent\\n provider: openai\\n model: gpt-4o-mini\\n maxTokens: 160\\n prompt: \\\"Write three markdown bullets about {input}.\\\"\"}}}") echo "$R" | mcptext WF_ID=$(echo "$R" | mcptext | uuid); echo "WF_ID=$WF_ID" ``` **C3** — run it, streamed as text. ```bash curl -sN -X POST "https://api.plungeai.com/v1/workflows/$WF_ID/execute-stream" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' -d '{"input":"the Rhine"}' | live ``` **C4** — CREATE a bot (`kind: bot`). ```bash R=$(mcp "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"plungeai_workflow\",\"arguments\":{\"user_request\":\"create\",\"action\":\"create\",\"kind\":\"bot\",\"name\":\"Quickstart bot $STAMP\",\"yaml\":\"workflow:\\n name: quickstart-bot\\n tasks:\\n - type: task\\n id: t1\\n agent: one-agent\\n provider: openai\\n model: gpt-4o-mini\\n maxTokens: 160\\n prompt: \\\"You are a friendly bot. Answer in markdown: {input}\\\"\"}}}") echo "$R" | mcptext BOT_ID=$(echo "$R" | mcptext | uuid); echo "BOT_ID=$BOT_ID" ``` **C5** — run the bot, streamed as text. ```bash curl -sN -X POST "https://api.plungeai.com/v1/workflows/$BOT_ID/execute-stream" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"input":"What should I pack for a week in Iceland in October?"}' | live ``` **C6** — CREATE an agentic agent (`kind: agent`): a harness mission on `one-agent` with a tool fence and an iteration cap. ```bash R=$(mcp "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":{\"name\":\"plungeai_workflow\",\"arguments\":{\"user_request\":\"create\",\"action\":\"create\",\"kind\":\"agent\",\"name\":\"Quickstart agentic agent $STAMP\",\"yaml\":\"workflow:\\n name: quickstart-agentic-agent\\n tasks:\\n - type: harness\\n id: run\\n agent: one-agent\\n goal: \\\"Explain {input} in three markdown bullets, then call task_complete with the bullets.\\\"\\n mission: \\\"You are a concise explainer. Answer in Markdown only.\\\"\\n allowed_tools: [task_complete]\\n max_iterations: 3\"}}}") echo "$R" | mcptext AGENT_ID=$(echo "$R" | mcptext | uuid); echo "AGENT_ID=$AGENT_ID" ``` **C7** — run the agentic agent, streamed. A harness run streams its engine events (`task_dispatched → task_completed → workflow_completed`); read the final Markdown from the results route with the `workflow_id` in the frames. ```bash curl -sN -X POST "https://api.plungeai.com/v1/workflows/$AGENT_ID/execute-stream" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"input":"how a harness bounds an agent run"}' | frames ``` **C8** — BUILD a workflow from a goal (`plungeai_build_workflow` — the LLM writes the CNL). ```bash R=$(mcp '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"plungeai_build_workflow","arguments":{"user_request":"build it","goal":"Search the web for the latest Cloudflare Workers news and summarise the top three items in markdown bullets"}}}') echo "$R" | mcptext BUILT_ID=$(echo "$R" | mcptext | uuid); echo "BUILT_ID=$BUILT_ID" ``` **C9** — run the built workflow, streamed. ```bash curl -sN -X POST "https://api.plungeai.com/v1/workflows/$BUILT_ID/execute-stream" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' -d '{"input":"Cloudflare Workers"}' | frames ``` **C10** — list what you created, delete it, prove it is gone. ```bash mcp '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"plungeai_list_workflows","arguments":{"user_request":"mine","search":"Quickstart "}}}' | mcptext for id in $WF_ID $BOT_ID $AGENT_ID $BUILT_ID; do mcp "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"tools/call\",\"params\":{\"name\":\"plungeai_workflow\",\"arguments\":{\"action\":\"delete\",\"workflow_id\":\"$id\"}}}" | mcptext done mcp '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"plungeai_list_workflows","arguments":{"user_request":"verify","search":"Quickstart "}}}' | mcptext ``` ## 1.6 The models plane with the OpenAI SDK The models plane is OpenAI-compatible and takes the same `ozk_` key as every other plane. Point the official SDK at `https://api.plungeai.com/v1` and pass a `provider/model` slug from `GET /v1/models`. ```python from openai import OpenAI client = OpenAI(api_key="<PLUNGE_API_KEY>", base_url="https://api.plungeai.com/v1") resp = client.chat.completions.create( model="anthropic/claude-sonnet-4-6", messages=[{"role": "user", "content": "Reply with exactly the word: pong"}], max_tokens=16, ) print(resp.choices[0].message.content) ``` ```ts import OpenAI from 'openai' const client = new OpenAI({ apiKey: process.env.PLUNGE_API_KEY, baseURL: 'https://api.plungeai.com/v1' }) const resp = await client.chat.completions.create({ model: 'anthropic/claude-sonnet-4-6', messages: [{ role: 'user', content: 'Reply with exactly the word: pong' }], max_tokens: 16, }) console.log(resp.choices[0].message.content) ``` ```bash curl -s https://api.plungeai.com/v1/chat/completions \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"model":"anthropic/claude-sonnet-4-6","messages":[{"role":"user","content":"Reply with exactly the word: pong"}],"max_tokens":16}' ``` The response `model` is the slug that actually served the request — after a failover it can differ from what you asked for. Add `"stream": true` for SSE. Ordered fallback (`models[]`), `sort`, `@preset/<slug>`, guardrails and the response cache are in [Models plane](/one-api/models). ## 1.7 Request ids and traces Every response echoes a server-minted `x-request-id`. Send your own UUID `x-trace-id` to correlate several calls, and read the persisted trace at `GET /v1/traces/{id}` ([Traces & observability](/one-api/traces-observability)). An id none of your calls used — or another account's — is `404 not_found`: ```bash curl -s "https://api.plungeai.com/v1/traces/$(uuidgen)" -H "Authorization: Bearer $PLUNGE_API_KEY" ``` Where to next: [Tools plane](/one-api/tools) for contracts and the trust fence, [Agents plane](/one-api/agents) for every body field, [Workflows plane](/one-api/workflows) for the CNL primer, [Streaming](/one-api/streaming) for the SSE vocabulary, [MCP plane](/one-api/mcp) to use the platform from Claude, Cursor or any MCP client. # Authentication & keys Source: https://www.plungeai.com/docs/one-api/authentication Every route except `GET /health` and `GET /v1/openapi.json` needs a key. One `ozk_` platform key opens every plane on `https://api.plungeai.com` (models, tools, agents, workflows, MCP, discovery, traces and the connector proxy) and the MCP server on `https://mcp.plungeai.com`. Two older prefixes, `sk-ocean-` and `sk-conn-`, are still accepted (legacy) on their own plane while integrations move over; new integrations use `ozk_`. ## The one key | Prefix | Name | Opens | Auth checked by | |---|---|---|---| | `ozk_` | Ocean account key | Every plane: models (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`), tools, agents, workflows, MCP, discovery, traces, and the connector proxy (`/v1/connect/:provider/proxy/*`) | `core/core-api-keys` — the execution planes through `orchestration/api-gateway/lib/auth.ts`, the models plane and the connector proxy through `inference/gateway/src/auth/api-key.ts` | In this guide: `$PLUNGE_API_KEY` = `ozk_`. Billing follows the account: a key carries your account id, so every metered call on it — models, embeddings, the connector proxy — is charged to your account's one wallet, whichever of your `ozk_` keys made it. Legacy keys: `sk-ocean-` (models plane only) and `sk-conn-` (connector proxy only) keep working on their own plane, are refused on the execution planes, and can no longer be created in the Dashboard. Replace them with an `ozk_` key. ## Which routes accept which key The router (`api-gateway.ts`) gates `/v1/discovery`, `/v1/tools`, `/v1/agents`, `/v1/workflows`, `/v1/mcp`, and `/v1/traces` with `requireAuth`, which accepts **only** a bearer that starts with `ozk_` (or a signed internal identity). The models plane and the connector proxy are mounted **without** `requireAuth` — the inference gateway resolves the key downstream, and it accepts the same `ozk_` key. A legacy `sk-ocean-` or `sk-conn-` key is accepted on its own plane only, so the one mistake left is a legacy key on an execution plane, which returns `401`. An `ozk_` key on the models plane works (live-verified): ```bash curl -s https://api.plungeai.com/v1/models \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` A legacy model-key shape on an execution plane → `401` (the router rejects any non-`ozk_` bearer before dispatch; live-verified): ```bash curl -s -o /dev/null -w '%{http_code}\n' -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer sk-ocean-EXAMPLE" \ # legacy model key -H "Content-Type: application/json" \ -d '{"prompt":"hi"}' ``` The `401` body is the standard error envelope with `X-Error-Code: unauthorized` (live-verified): ```bash curl -s https://api.plungeai.com/v1/tools ``` ## Header forms Both header forms work on the execution planes; the models plane (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`) takes `Authorization: Bearer` only — `X-API-Key` there is a `401`, by design. | Header | Planes | Example | |---|---|---| | `Authorization: Bearer <key>` | all | `-H "Authorization: Bearer $PLUNGE_API_KEY"` | | `X-API-Key: <key>` | execution planes (`ozk_`); **not** the models plane | `-H "X-API-Key: $PLUNGE_API_KEY"` | `X-API-Key` with an `ozk_` key is live-verified: ```bash curl -s "https://api.plungeai.com/v1/tools?limit=1" \ -H "X-API-Key: $PLUNGE_API_KEY" ``` The same header on the models plane → `401` (live-verified): ```bash curl -s -o /dev/null -w '%{http_code}\n' https://api.plungeai.com/v1/models \ -H "X-API-Key: $PLUNGE_API_KEY" ``` ## How to obtain each key | Key | Where | Notes | |---|---|---| | `ozk_` | Dashboard → **One API → Keys** (`https://dashboard.plungeai.com`) | Self-service; session-authenticated (`apps/ocean-dashboard/src/worker/routes/platform-keys.routes.ts`, `POST /api/platform-keys`). Name the key, pick an expiry (never / 7–365 days), copy it once — it is shown exactly once and stored hashed. Bound to your account and its tier; revoke any time from the same list. | The tab shows your **Ocean account key** (prefix and last four characters; **Rotate** replaces it) — the key the Playground, `ocean-cli` and OceanCode use — above any extra named keys you create there. The command-line clients get the account key through a browser sign-in, with no key to paste: `ocean-cli login` and `oceancode login --provider ocean` listen on `127.0.0.1:<port>` and open `https://dashboard.plungeai.com/api/cli/authorize?port=<port>&state=<state>`. Once you are signed in (any sign-in method), the Dashboard redirects a one-time code (valid 120 s) to `http://127.0.0.1:<port>/callback`, and the client exchanges it with `POST https://dashboard.plungeai.com/api/cli/token` for the key. The key never appears in a browser URL. Existing `sk-ocean-` and `sk-conn-` keys are listed read-only in the same tab under **Legacy keys**; each can be revoked there, and no new ones are created. ## Key fences (scoping) Keys can be scoped so a leak is bounded. | Fence | Key kind | Effect | Source | |---|---|---|---| | `allowed_tools` | `ozk_` | Restrict the key to a named set of tools | `orchestration/mcp-gateway/auth-middleware.ts` | | `allowed_ips` | `ozk_` | Accept the key only from listed IPv4 addresses / CIDR blocks or exact IPv6 addresses; a call from elsewhere is a definitive `401` (no fallback) | `core/core-api-keys/index.ts` (`ipAllowed`, `verifyApiKeyRecord`) | | `allowed_models` | `sk-ocean-` (legacy) | Restrict the key to a model allow-list; an off-list model is `403 model_not_allowed` | `inference/gateway/src/auth/api-key.ts` (`assertModelAllowed`) | | `monthly_limit_usd` | `sk-ocean-` / `sk-conn-` (legacy) | Per-key monthly spend cap; exceeding it is `429 insufficient_quota` (inference) or `402 insufficient_balance` (connector) | `inference/gateway/src/auth/rate-limit.ts` | `ozk_` key records are stored hashed only (`key:{sha256}` in KV, `core/core-api-keys`); a named key is shown once at mint time. The account key's plaintext is also kept encrypted (`ocean-compliance.user_credentials`) so the Dashboard can hand it to your signed-in clients. ## Tiers and rate limits Execution-plane rate limiting is per-key (per user uuid), enforced by a Durable Object on `POST` executions only — catalog `GET`s are free (`api-gateway.ts` calls `checkRateLimit` for `POST` only). The limits below are the code values in `orchestration/mcp-gateway/rate-limiter.ts`: | Tier | Requests / minute | Requests / day | |---|---|---| | free | 30 | 1,000 | | pro | 100 | 10,000 | | enterprise | 300 | 100,000 | Reaching the day limit pauses the key for **ten minutes** (`Retry-After` ≤ 600), after which its day count starts over — a key is never shut out longer than that (2026-10-05); the minute limit waits out the minute. A key with no tier is treated as `free`: the key resolver defaults it before the limiter sees it (`orchestration/mcp-gateway/auth-middleware.ts` `validateApiKey`). A per-key `rate_limit` override is **ignored** on these planes (`orchestration/api-gateway/lib/auth.ts` does not pass it to `checkRateLimit`); it applies only on `https://mcp.plungeai.com/v1`. The numbers above are the defaults. PlungeAI sets the per-tier numbers, or switches the limits off, in the operator console (One API › Keys › Rate limits; KV `rate-limits:config`, read by the limiter within a minute); while they are off no call is counted and none returns `429`, and the failed-authentication meter in 13.4 stays on. The models plane has its own limiter — a default of **600 req/min per key** (`RATE_LIMIT_RPM_DEFAULT`, `inference/gateway/src/routes/chat-completions.ts`), also overridable per key. A separate **failed-auth limiter** meters bad credentials at **20 attempts / minute per client IP** (`auth-fail` tier in `rate-limiter.ts`; wired in `auth.ts`) so key enumeration is bounded. When you are limited, the response is `429` with a `Retry-After` header (seconds) and a `rate_limited` (execution planes) or `rate_limit_exceeded` (models plane) error code. ## Request ids and trace correlation - **`x-request-id`** is minted server-side on every request and echoed on the response (`api-gateway.ts`). It is never trusted from the client. Include it when reporting a problem. Live-verified — the header is present: ```bash curl -sD - -o /dev/null https://api.plungeai.com/health | grep -i '^x-request-id' ``` - **`x-trace-id`** is yours to set for correlating several calls into one trace. It is honoured **only if it is a UUID**; otherwise the router falls back to the request id. On an execution route the trace id doubles as the execution/workflow primary key, so replaying a UUID that was already used returns `409 duplicate_execution_id` — send a fresh UUID or omit the header. Responses also set `X-Content-Type-Options: nosniff`. Error responses additionally set `X-Error-Code` (equal to `error.code`) so a client can branch on the header regardless of the negotiated body format. ## CORS The router answers CORS for any origin and exposes the response headers a browser client needs (`x-request-id`, `X-Error-Code`, `Retry-After`, …; `cors()` in `api-gateway.ts`), and the models plane sets `access-control-allow-origin: *` with `authorization, content-type, x-request-id` allowed and `x-request-id, x-correlation-id, x-ocean-cost-usd, x-ocean-dropped-params, x-generation-id` exposed (`inference/gateway/src/index.ts`). Browser SDKs work, but do not ship a secret key in client-side code. ## Internal HMAC identity (not for customers) Service-to-service calls inside the platform authenticate with a signed internal identity (`X-Internal-Signature`, HMAC over the request) instead of a key, verified before the `ozk_` path in `auth.ts`. This is the mechanism Ocean Studio and the engine use to call each other; it is not available to external developers and needs no action from you. # Models plane Source: https://www.plungeai.com/docs/one-api/models The models plane is the OpenAI-compatible surface: `POST /v1/chat/completions`, `POST /v1/embeddings`, `GET /v1/models`. Base URL `https://api.plungeai.com`. Auth is your `ozk_…` account key in `Authorization: Bearer` (only: `X-API-Key` is a `401` on this plane, except on `/v1/messages`, [3.16](#316-anthropic-messages-post-v1messages)), billed to your account's wallet — see [Authentication & keys](/one-api/authentication). `api.plungeai.com/v1/*` is a transparent proxy (`orchestration/api-gateway/routes/models-proxy.ts`) to the inference gateway (`inference/gateway/src/`); the proxy adds no auth of its own — the inference gateway owns the key, wallet, and metering. > Examples in this chapter that carry `# verify` are run against the deployed API with an `ozk_` key by `verify-examples.mjs`; the others are verified against the gateway source (`inference/gateway/src`). ## 3.1 Endpoints | Method | Path | Body | Notes | |---|---|---|---| | POST | `/v1/chat/completions` | JSON | Sync or `stream:true` SSE. Full field reference in [3.2](#32-request-fields). Handler `routes/chat-completions.ts`. | | POST | `/v1/messages` | JSON | Anthropic Messages wire on the same money path; `x-api-key` or Bearer. Sync or `stream:true` SSE ([3.16](#316-anthropic-messages-post-v1messages)). Handler `routes/messages.ts`. | | POST | `/v1/completions` | JSON | Legacy text completion: `prompt` is one string (arrays answer `400 unsupported_parameter`); answered as `text_completion`, streamed or not. A chat request with one user message underneath — same routing, guardrails, cache and billing. Handler `routes/completions.ts`. | | POST | `/v1/embeddings` | JSON | OpenAI, Mistral, Google and Cohere models ([3.9](#39-embeddings)). Handler `routes/embeddings.ts`. | | GET | `/v1/models` | — | Aggregate catalog, OpenAI shape ([3.8](#38-get-v1models)). Requires a key. Handler `routes/models.ts`. | | GET | `/v1/generation` | — | Usage and cost of one completion ([3.15](#315-account--generation-routes)). Handler `routes/account.ts`. | | GET | `/v1/credits` | — | Lifetime credits bought and used ([3.15](#315-account--generation-routes)). | | GET | `/v1/key` · `/v1/auth/key` | — | The calling key: label, limit, usage, rate limit ([3.15](#315-account--generation-routes)). | | GET | `/v1/models/{author}/{slug}/endpoints` | — | The endpoint serving one model ([3.15](#315-account--generation-routes)). | ## 3.2 Request fields The support words below are: **supported** (mapped and sent), **partial** (only part works — the cell says which), **ignored** (accepted, no effect), **rejected** (errors), **Ocean-only** (a field OpenAI does not define). Every request routes to exactly one **provider shape**. openai-shape providers: **openai, groq, mistral, deepseek, xai, together, fireworks, cerebras, perplexity, openrouter, moonshot** (`routing/model-router.ts` `PROVIDERS`). For **openai-shape** providers the gateway forwards your body after one field split (`splitRequestFields`, `routing/params.ts`): Ocean directives (`models`, `sort`, `compress`) and OpenRouter-only fields (`provider`, `route`, `transforms`, `plugins`, `usage`, `reasoning`, `include_reasoning`, `debug`, `session_id`) are removed before the provider sees them (`reasoning.effort` is sent as `reasoning_effort`). The sampling knobs `top_k`, `min_p`, `top_a` and `repetition_penalty` are forwarded to the OpenAI-compatible hosts only: **OpenAI itself** never receives them, nor any other field it does not define, while the other openai-shape providers keep provider-specific extras and `top_k`. Those names are returned in the `x-ocean-dropped-params` response header ([3.6](#36-response-headers)) — except `min_p`, `top_a` and `repetition_penalty`, which OpenRouter also defines and which are removed without a mention, like the other OpenRouter-only fields. For **anthropic**, **google** and **cohere** the gateway builds a fresh provider body from the same split; a field the shape cannot take is dropped (**ignored**). Those names are returned in the `x-ocean-dropped-params` response header ([3.6](#36-response-headers)) — OpenRouter-only fields (`min_p`, `top_a`, `repetition_penalty` among them) are again removed without a mention. ### Core fields | Field | openai-shape | anthropic | google | cohere | Source / note | |---|---|---|---|---|---| | `model` | supported | supported | supported | supported | `provider/model` slug or bare alias ([3.7](#37-providers--slugs)). Required unless `models`. | | `models` (array) | Ocean-only | Ocean-only | Ocean-only | Ocean-only | Ordered fallback list; **stripped** before an openai-shape upstream. `routing/select.ts` `resolveCandidates` ([3.3](#33-routing-models-sort-failover)). | | `sort` | Ocean-only | Ocean-only | Ocean-only | Ocean-only | `price` \| `latency` \| `throughput`; stripped before upstream. `routing/select.ts` `sortCandidates`. | | `messages` | supported | supported | supported | supported | Required; empty → `400 invalid_request_error` "messages required". | | `stream` | supported | supported | supported | supported | SSE ([Streaming](/one-api/streaming)). | | `max_tokens` | supported | supported | supported | supported | Defaults to **4096** on every shape when both token caps are omitted (`DEFAULT_MAX_OUTPUT_TOKENS_ESTIMATE`). For reasoning models (`o1..o9`, `gpt-5*`) it is folded into `max_completion_tokens`. | | `max_completion_tokens` | supported | supported | supported | supported | Preferred for reasoning models; on translated shapes it feeds the same output cap. | | `temperature` | supported | supported | supported | supported | Mapped on every shape. | | `top_p` | supported | supported (`top_p`) | supported (`topP`) | supported (`p`) | | | `stop` | supported | supported (`stop_sequences`) | supported (`stopSequences`) | supported (`stop_sequences`) | String or array. | | `n` | supported (passthrough) | ignored | ignored | ignored | Reserve is sized for `n` completions; only openai-shape returns multiple choices. | | `stream_options` | supported (passthrough) | ignored | ignored | ignored | Controls `include_usage` for openai-shape; translated shapes **always** emit a final `usage` ([4](/one-api/streaming)). | | `user` | supported (passthrough) | supported | ignored | ignored | anthropic → `metadata.user_id`. | ### Messages & content parts | Field | openai-shape | anthropic | google | cohere | Source / note | |---|---|---|---|---|---| | content part `text` | supported | supported | supported | supported | | | content part `image_url` | supported (passthrough) | partial | supported | ignored | anthropic forwards the raw OpenAI part **unconverted** (`toAnthropicBody` passes non-text parts as-is) — real Anthropic expects `{type:"image",source:…}` and may 400. google: a data URI → `inlineData`, an http(s) URL → `fileData`. cohere extracts text parts only. | | content part `file` / `input_audio` | supported (passthrough) | partial | ignored | ignored | Forwarded raw to anthropic, dropped for google/cohere. | | `name` (on a message) | supported (passthrough) | ignored | ignored | ignored | Dropped by every translator. | | `role:"tool"` message | supported (passthrough) | supported (mapped) | supported (mapped) | ignored | anthropic: `tool_result` / `tool_use` blocks; the results of one call share one user turn. google: `functionResponse` / `functionCall` parts. cohere: text-only extraction. | | `tool_call_id` | supported (passthrough) | supported (mapped) | supported (mapped) | ignored | anthropic: `tool_result` / `tool_use` blocks; the results of one call share one user turn. google: `functionResponse` / `functionCall` parts. cohere: text-only extraction. | | `tool_calls` (assistant) | supported (passthrough) | supported (mapped) | supported (mapped) | ignored | anthropic: `tool_result` / `tool_use` blocks; the results of one call share one user turn. google: `functionResponse` / `functionCall` parts. cohere: text-only extraction. | ### Tools & structured output | Field | openai-shape | anthropic | google | cohere | Source / note | |---|---|---|---|---|---| | `tools` | supported (passthrough) | supported (mapped) | supported (mapped) | ignored | anthropic → `{name,description,input_schema}`; google → `functionDeclarations`; **cohere drops tools** (`toCohereBody` emits none). | | `tool_choice` | supported (passthrough) | supported | supported (mapped) | ignored | anthropic: `auto`, `none`, `required`→`any`, a named function→`tool`. google → `toolConfig`. | | `parallel_tool_calls` | supported (passthrough) | ignored | ignored | ignored | | | `response_format` | supported (passthrough) | supported | supported (mapped) | ignored | anthropic: `json_object` / `json_schema` through one forced synthetic tool; ignored when the request also sends `tools`. google → `responseMimeType` + `responseSchema`. Not mapped for cohere. | A forced tool call on the Anthropic shape comes back as `tool_calls` with `finish_reason:"tool_calls"` and `arguments` `{"city":"Berlin"}` (live-verified 2026-10-04). The call needs about 33 completion tokens, above the 16-token cap of `# verify` fences, so it carries no marker: ```bash curl -s "$API_BASE/v1/chat/completions" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "content-type: application/json" \ -d '{"model":"anthropic/claude-haiku-4-5","max_tokens":64,"messages":[{"role":"user","content":"Weather in Berlin?"}],"tools":[{"type":"function","function":{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}' ``` A `json_schema` response on the Google shape. Gemini 2.5 counts its thinking tokens against `max_tokens`, so the call needs `512` and carries no `# verify` marker; `message.content` is the JSON string `{"a":1}`: ```bash curl -s "$API_BASE/v1/chat/completions" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "content-type: application/json" \ -d '{"model":"google/gemini-2.5-flash","max_tokens":512,"messages":[{"role":"user","content":"Return a=1"}],"response_format":{"type":"json_schema","json_schema":{"name":"o","schema":{"type":"object","properties":{"a":{"type":"integer"}},"required":["a"]}}}}' ``` ### Sampling & OpenRouter extensions | Field | openai-shape | anthropic | google | cohere | Source / note | |---|---|---|---|---|---| | `top_k` | supported on openai-compatible hosts, **ignored** on OpenAI itself | supported | supported (mapped) | ignored | google → `topK`. | | `presence_penalty`, `frequency_penalty` | supported (passthrough) | ignored | supported (mapped) | ignored | google → `presencePenalty` / `frequencyPenalty`. | | `repetition_penalty` | supported on openai-compatible hosts, **ignored** on OpenAI itself | ignored | ignored | ignored | | | `seed` | supported (passthrough) | ignored | supported (mapped) | ignored | google → `seed`. | | `logit_bias`, `logprobs`, `top_logprobs` | supported (passthrough) | ignored | ignored | ignored | Forwarded verbatim on openai-shape; the upstream decides. | | `min_p`, `top_a` | supported on openai-compatible hosts, **ignored** on OpenAI itself | ignored | ignored | ignored | | | `prediction`, `modalities`, `verbosity`, `web_search_options` | supported (passthrough) | ignored | ignored | ignored | openai-shape passthrough only. | | `reasoning`, `reasoning_effort` | supported (passthrough) | ignored | ignored | ignored | Forwarded to reasoning-capable openai-shape providers. On the [Agents plane](/one-api/agents) `reasoning_effort` is a first-class field; here it is passthrough only. | | `usage` (request-level) | supported | supported | supported | supported | `usage:{include:true}` adds `usage.cost` to the response ([3.5](#35-response-object)); Ocean always meters. | | `provider` | ignored | ignored | ignored | ignored | OpenRouter-only. Accepted and removed before the provider; use Ocean `models` / `sort` for routing. | | `route` | ignored | ignored | ignored | ignored | OpenRouter-only. Accepted and removed before the provider; use Ocean `models` / `sort` for routing. | | `transforms`, `plugins` | ignored | ignored | ignored | ignored | OpenRouter-only. Accepted and removed before the provider; use Ocean `models` / `sort` for routing. | | `prompt` (legacy) | rejected | rejected | rejected | rejected | Chat endpoint requires `messages` — a body without them is `400` "messages required". | ## 3.3 Routing: models, sort, failover `routing/select.ts` and the `runAttempts` loop in `routes/chat-completions.ts`. - **`models[]`** — an ordered candidate list. Each entry is normalized (alias table + `provider/model` shape); an unresolvable entry is **dropped** (recorded as `reason:"unknown_model"`), not fatal, as long as one survives. If every entry is unknown → `404 model_not_found`. - **`sort`** — reorders candidates: `price` (cheapest `input+output` rate first, from the rate card), `latency` (lowest rolling p50, unknown providers last), `throughput` (highest rolling tok/s first). No `sort` or `<2` candidates → input order preserved. - **Attempt loop** — one candidate at a time: up to **2 attempts** per candidate (one retry on `429`/`5xx`/network), then **failover** to the next. A non-429 `4xx` is a **caller error** on that candidate — no retry, and the loop moves to the next candidate (a retired model id or a stale key on one provider does not sink the request); when no candidate succeeds, the first such error's status and body are returned, so a single-model request gets its own `4xx` unchanged. Exhausting every candidate → `502` "All routing candidates failed". - **Outage exclusion** — a candidate that exhausts its retries against a real upstream failure is marked out for **30s** (KV `outage:<provider>`); subsequent requests skip it (`reason:"outage_excluded"`) until the window elapses. - **Retry backoff** — honors a small upstream `Retry-After` (capped at 2s), else a flat 250ms. - **Honest `model`** — the response's `model` is the slug that actually **served** the request (matters after failover), not the one you asked for, for anthropic/google/cohere and for the **non-stream** openai path. On the **openai-shape stream** the per-chunk `model` is left as the upstream reports it (not rewritten). The full ordered trail is persisted to `inference_requests.route_attempts`. ## 3.4 Presets Set `model` to `@preset/<slug>` (`policy/presets.ts`). The gateway resolves a stored preset (org-specific row wins over a platform-global one) and merges it **before** candidate resolution. Precedence: **your explicit request fields always win** — preset `params_json` fills defaults, preset `routing_json.models`/`sort` apply only when you did not send `models`/`sort`; the decision log records `@preset/<slug>` as the requested model. Unknown or inactive slug → `404 preset_not_found`. Presets are provisioned by an operator (signed `/admin/presets`); there is no public create route. ## 3.5 Response object Non-stream body is OpenAI `chat.completion`; stream chunks are `chat.completion.chunk` ([4](/one-api/streaming)). | Field | Value | |---|---| | `id` | `chatcmpl-…` on every shape (openai passthrough keeps the upstream id; translators mint one). | | `object` | `chat.completion` \| `chat.completion.chunk`. | | `created` | Unix seconds (upstream's for openai passthrough; `Date.now()` for translated shapes). | | `model` | The served slug (see honest-model note in [3.3](#33-routing-models-sort-failover)). | | `provider` | canonical provider id that served the request — `openai`, `anthropic`, …; every response and every stream chunk | | `choices[].message` / `.delta` | Standard OpenAI shape. | | `choices[].finish_reason` | See vocabulary below. | | `choices[].native_finish_reason` | the provider's own finish reason — `end_turn`, `STOP`, `TOOL_CALL`, …; on openai-shape providers it equals `finish_reason` | | `usage` | `prompt_tokens`, `completion_tokens`, `total_tokens`, plus `prompt_tokens_details.cached_tokens` and Ocean's extension `prompt_tokens_details.cache_write_tokens` when a cache read/write occurred. | | `usage.cost`, `usage.is_byok`, `usage.cost_details` | only with `usage:{"include":true}`: `cost` is the price charged in USD, `cost_details.upstream_inference_cost` the list cost at markup 1, split into `upstream_inference_prompt_cost` and `upstream_inference_completions_cost` | **`finish_reason` per shape** (`mapAnthropicStop` / `mapGoogleStop`, `streaming/sse-translate.ts`): | Upstream | openai | anthropic | google | cohere | |---|---|---|---|---| | normal end | `stop` | `end_turn`,`stop_sequence`→`stop` | `STOP`→`stop` | always `stop` | | length cap | `length` | `max_tokens`→`length` | `MAX_TOKENS`→`length` | `stop` | | tool call | `tool_calls` | `tool_use`→`tool_calls` | a `functionCall` part → `tool_calls` | `stop` | | content filter | `content_filter` | (mapped to `stop`) | `SAFETY`,`RECITATION`→`content_filter` | `stop` | **Usage & cache tokens** — Anthropic's `input_tokens` (which *excludes* cache) is normalized to OpenAI semantics: `prompt_tokens` includes the cached portion, and `prompt_tokens_details` breaks it out. Cached tokens bill at cache rates and are subtracted from full-rate input, never double-counted (`metering/pricing.ts computeCost`). ## 3.6 Response headers | Header | When | Meaning | |---|---|---| | `x-request-id` | always | The inference gateway's request id — the same value as `error.request_id` and the id of your row in the request log. The client's own `x-request-id` is never trusted as an id. | | `x-ocean-router-id` | via `api.plungeai.com` | The router's own id for the call (its request-log row); quote it to support next to `x-request-id`. | | `x-correlation-id` | when you send `x-request-id` | Echoes your value. | | `x-cache` | cache-eligible + org cache on | `hit` (served from cache, $0) or `miss` (stored for next time) ([3.11](#311-response-cache)). | | `x-ocean-cost-usd` | non-stream chat completions | The settled price in USD, 6 decimals (`0.000000` on a cache hit). Not sent on a stream: its price exists only after the last chunk. | | `x-ocean-dropped-params` | when the request carried fields the routed provider shape cannot take | comma-separated names that were removed (see [3.2](#32-request-fields)); absent otherwise | | `content-type` | always | `application/json` or `text/event-stream`. | The money-plane error body carries `request_id` inside the JSON envelope; note it does **not** set `X-Error-Code` (that header is execution-plane only — see [Errors, limits & headers](/one-api/errors-limits)). ## 3.7 Providers & slugs Address a model as `provider/model` (e.g. `anthropic/claude-sonnet-4-6`). Bare aliases in `routing/model-router.ts` `ALIASES` auto-prefix: `gpt-4o`, `gpt-4o-mini`, `gpt-4.1`, `gpt-4.1-mini`, `o1`, `o1-mini`, `o3-mini`, `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5`, `gemini-2.5-pro`, `gemini-2.5-flash`. Any other bare name (no `/`) → `404 model_not_found`. | Provider id | Shape | Upstream | |---|---|---| | `openai` | openai | api.openai.com | | `anthropic` | anthropic | api.anthropic.com/v1/messages | | `google` | google | generativelanguage.googleapis.com | | `groq`, `mistral`, `deepseek`, `xai`, `together`, `fireworks`, `cerebras`, `perplexity`, `moonshot` | openai | each provider's OpenAI-compatible endpoint | | `openrouter` | openai | openrouter.ai/api/v1 | | `cohere` | cohere | api.cohere.com/v2/chat | **Discover the live catalog** with `GET /v1/models` ([3.8](#38-get-v1models)) or the discovery plane `GET /v1/discovery/search?kind=models` ([Discovery](/one-api/discovery)). Not every provider slug in the map is priced/active — the catalog is the source of truth. ## 3.8 GET /v1/models Requires a key. Returns `{ "object":"list", "data":[ … ] }` built from the rate-card snapshot (`routes/models.ts`, `metering/pricing.ts listCatalog`). Each entry: ```json { "id": "anthropic/claude-sonnet-4-6", "object": "model", "created": 0, "owned_by": "anthropic", "context_length": 200000, "max_output": 8192, "pricing": { "in": 3.0, "out": 15.0 }, "capabilities": { "streaming": true, "tools": true, "vision": true, "embedding": false, "fim": false }, "name": "Anthropic: claude-sonnet-4-6", "canonical_slug": "anthropic/claude-sonnet-4-6", "architecture": { "modality": "text+image->text", "input_modalities": ["text","image"], "output_modalities": ["text"], "tokenizer": null, "instruct_type": null }, "pricing_per_token": { "prompt": "0.000003", "completion": "0.000015", "input_cache_read": "0.0000003", "input_cache_write": "0.00000375" }, "top_provider": { "context_length": 200000, "max_completion_tokens": 8192, "is_moderated": false }, "supported_parameters": ["temperature", "top_p", "max_tokens", "max_completion_tokens", "stop", "tools", "tool_choice", "response_format", "top_k"], "per_request_limits": null } ``` `pricing.in`/`.out` are USD **per 1M tokens**. `pricing_per_token` is the same price in OpenRouter's format (USD per token, decimal string); `supported_parameters` lists what the routed provider shape honours (tool parameters only for tool-capable models). `vision:false` means "not verified", not "verified absent"; `max_output:null` means unknown; `fim` is a name heuristic (`codestral`). This shape differs from OpenRouter's `/models` — see [OpenRouter compatibility](/one-api/openrouter-compat). ## 3.9 Embeddings `POST /v1/embeddings` serves **openai**, **mistral**, **google** and **cohere** models (`routes/embeddings.ts`, `streaming/embeddings-translate.ts`); any other provider answers `400 unsupported_provider`. Body is standard OpenAI (`model`, `input` string or array, optional `encoding_format`: `float` or `base64`); the answer is always OpenAI's `{object:"list", data:[{embedding, index}], model, usage}`, whatever the provider speaks. When a provider reports no token count (google) the usage is estimated as characters/4. An upstream failure passes through with its own status. BYOK, guardrails, reserve/settle metering, and trial exemption apply exactly as for chat. The price is the rate card's per-token input price; `google/gemini-embedding-001` is served (live-verified 2026-10-04) but has no rate-card row yet, so `GET /v1/models` does not list it and no price is published for it. ## 3.10 Guardrails Org- and key-scoped policies (`policy/guardrails.ts`), merged strictest-wins and enforced **before** any spend or upstream call. What a developer sees: | Situation | Status | Code | |---|---|---| | Model/provider not on the allow-list (or key `allowed_models`) | `403` | `model_not_allowed` | | Prompt matches a blocked-content regex | `403` | `content_blocked` | | Provider requires your own key on this plan and none is connected | `402` | `byok_required` | | Org day/week/month spend cap reached | `429` | `spend_cap_exceeded` | Spend caps count inference **and** connector spend plus outstanding wallet holds. On a `models[]` request the allow-list/`require_byok` filters narrow the candidate list per entry; only if **none** survive do you get the error above. ## 3.11 Response cache Exact-match, per-org opt-in, **default off** (`metering/response-cache.ts`). Eligibility: `stream:false` **and** an explicit `temperature:0` (an absent temperature defaults to 1 and is not eligible). The key is a SHA-256 of the **entire** request body scoped by org, so any differing field (tools, `response_format`, `seed`, …) is a different entry. A **hit** returns the stored body with `x-cache: hit`, bills **$0** (no wallet touch, no upstream call); a **miss** on an eligible request sets `x-cache: miss` and stores the response (TTL clamped 60s–24h). A hit can never cross an org boundary. ## 3.12 BYOK-or-billed Connect your own provider key (BYOK) in the dashboard; it is stored AES-256-GCM per org (`routing/byok.ts`, table `inference_byok_keys` on `ocean-billing`). When a BYOK key exists for the winning candidate's provider, the gateway uses it and bills only the org's flat `byok_routing_fee` (`cost=0`, `price=byok_fee`). A **single-candidate** BYOK request with a **zero** routing fee skips the wallet entirely (no reserve/settle/ledger row); a multi-candidate request always reserves (the winner may not be the zero-fee one). Guardrail `require_byok` can force BYOK for named providers → `402 byok_required` if unmet. ## 3.13 Billing & metering From `inference/CHARGING-README.md`. **Every request is metered; only `active` accounts ever pay** — an unknown/missing/stale status resolves to `trial` (metered, never debited). | Status | Calls | Wallet | Monthly cap | |---|---|---|---| | `trial` | yes | never debited | exempt; a per-org **$10/mo** platform-cost allowance applies (`TRIAL_MONTHLY_CAP_USD`), then `429 insufficient_quota` | | `active` | yes | reserve→settle at true price | enforced | | `suspended` / `closed` | `401` at key resolution | — | — | - **Price** = provider `cost × org markup` (`inference_org_settings`), computed from the rate-card components (`metering/pricing.ts`); component-based, per-1M normalized. - **Reserve-then-settle** — a conservative max estimate is held before the upstream call and settled at the true price after (`metering/billing.ts`). A failure releases the hold (no charge). A near-empty wallet → `402 insufficient_balance`. An `active` key over its `monthly_limit_usd` → `429 insufficient_quota`. - **RPM limit** applies to everyone (abuse protection, not billing): `429 rate_limit_exceeded` at `RATE_LIMIT_RPM_DEFAULT` (600/min default), atomic per key ([Errors, limits & headers](/one-api/errors-limits)). **Posted prices.** A model with a posted Ocean price bills at that price whichever host serves it; `x-ocean-pricing` on every completion says `posted`, `passthrough` (cost × markup, above) or `byok`, and `x-ocean-provider` names the host. A posted price never exceeds the model maker's own list price; a published change reaches billing within 120 s; an aborted stream settles what streamed at the posted price. The public list: `GET /v1/pricing` (no key), live at https://plungeai.com/pricing/. ## 3.14 Not supported today - No `/v1/responses` (Responses API), `/v1/messages/count_tokens` or Message Batches (the Anthropic Messages wire itself is [3.16](#316-anthropic-messages-post-v1messages)), `/v1/providers`, `/v1/images`, `/v1/audio`, `/v1/moderations` or `/v1/rerank`, and no `/v1/embeddings/models` list route. - `/v1/embeddings` serves openai, mistral, google and cohere models only ([3.9](#39-embeddings)); any other provider is `400 unsupported_provider`. - `/v1/completions` takes `prompt` as one string; an array is `400 unsupported_parameter`. - `usage.completion_tokens_details.reasoning_tokens` is passed through only when an openai-shape provider sends it; the anthropic, google and cohere translations do not report it. - A request that times out is not answered with `408`: a network failure fails over to the next candidate and ends as `502` when every candidate failed. - A `service_tier` other than `auto` or `default` is `400 unsupported_service_tier` (live-verified 2026-10-04): the rate card prices the default tier only. - The OpenRouter-only request fields `provider`, `route`, `transforms`, `plugins`, `include_reasoning`, `debug` and `session_id` are accepted and **ignored**: they are removed before the provider sees them, without a mention in `x-ocean-dropped-params`. Use Ocean `models` / `sort` / presets for routing. `min_p`, `top_a` and `repetition_penalty` reach the OpenAI-compatible hosts only (never OpenAI itself, anthropic, google or cohere). - On **anthropic**, `n`, `seed`, `presence_penalty`, `frequency_penalty`, `logit_bias`, `logprobs` / `top_logprobs`, `parallel_tool_calls` and `reasoning_effort` are dropped (the Messages API has no such fields) and named in `x-ocean-dropped-params`; `response_format` is not enforced when the request also sends `tools`; images are forwarded without conversion (may `400`). - On **google**, `n`, `logit_bias`, `logprobs` / `top_logprobs`, `parallel_tool_calls` and `reasoning_effort` are dropped and named in `x-ocean-dropped-params`, and so are `file` / `input_audio` content parts (images are mapped). - On **cohere**, the translation is text-only: `tools`, `tool_choice`, `response_format`, `top_k`, `seed`, `presence_penalty` / `frequency_penalty` and images are dropped **without** a mention in `x-ocean-dropped-params` (Cohere tool calling is not built); the fields listed for google are dropped and named. ```bash curl -s "$API_BASE/v1/chat/completions" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "content-type: application/json" \ -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":8}' ``` The models plane takes the same `ozk_` key as every other plane; a missing or invalid key is `401 invalid_api_key` (a legacy `sk-ocean-` key is also accepted here). `GET /v1/models` enforces the same: ```bash curl -s "$API_BASE/v1/models" -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ## 3.15 Account & generation routes Four read-only routes answer for the calling key's own account. They live on `/v1/*` (OpenAI shape, key required) and `/api/v1/*` (OpenRouter shape; `/models/{author}/{slug}/endpoints` is public there, like OpenRouter's). | Route | Answers | Read from | |---|---|---| | `GET /generation?id=<id>` | one completion's usage and cost: `tokens_prompt`, `tokens_completion`, `native_tokens_cached`, `total_cost` (what you were charged, USD), `upstream_inference_cost`, `is_byok`, `generation_time`, `latency`. `id` is the `gen-…` id of an `/api/v1` response (also its `x-generation-id` header) or the `x-request-id` of any call. Another account's id answers `404 not_found`. | `inference_requests` | | `GET /credits` | `{"data":{"total_credits","total_usage"}}` — lifetime USD bought and spent; zeros for an account with no wallet yet | `inference_org_balance` | | `GET /key` (alias `/auth/key`) | `{"data":{"label","limit","usage","limit_remaining","is_free_tier","rate_limit":{"requests","interval"}}}` — `usage` is this UTC month's spend on this key | `inference_api_keys` (legacy `sk-ocean-`), the key record (`ozk_`) | | `GET /models/{author}/{slug}/endpoints` | the one endpoint that serves a model, with per-token pricing | rate card | A generation row is written after the response completes, so a lookup in the same second can answer 404 — retry after a moment. ```bash curl -s "$API_BASE/api/v1/credits" -H "Authorization: Bearer $PLUNGE_API_KEY" ``` The `gen-` id of an `/api/v1` completion resolves through `GET /v1/generation` (live-verified 2026-10-04): ```bash RID=$(curl -s "$API_BASE/api/v1/chat/completions" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "content-type: application/json" \ -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":4}' \ | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])') sleep 3 curl -s "$API_BASE/v1/generation?id=$RID" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ## 3.16 Anthropic Messages (`POST /v1/messages`) The Anthropic Messages wire on the same money path as `/v1/chat/completions` (routing, `models`/`sort` failover, guardrails, reserve and settle, the `x-ocean-*` headers of [3.6](#36-response-headers)). An Anthropic SDK or Claude Code points its base URL at the gateway and keeps its own request and response shapes. Handler `routes/messages.ts`; translation `ingress/anthropic-in.ts` / `ingress/anthropic-out.ts`. Also mounted at `/api/v1/messages` (OpenRouter's Anthropic skin, `fallbacks` up to 3). - **Auth:** `x-api-key: ozk_…` (with any `anthropic-version`) or `Authorization: Bearer ozk_…`. - **Request:** `model` (any slug, not only `anthropic/*`), `messages` (string or blocks: `text`, `image`, `tool_use`, `tool_result`, `thinking`, …), `system` (string or blocks), `max_tokens`, `stop_sequences`, `temperature`, `top_p`, `top_k`, `tools`, `tool_choice`, `metadata.user_id`, `thinking`, `stream`; plus Ocean's `models`, `sort`, `max_cost`. - **`anthropic/*` models:** your body is forwarded as sent, with only `model` (the upstream id) and `max_tokens` (the capped value, [3.2](#32-request-fields)) set by the gateway — `cache_control`, thinking and server tools reach Anthropic unchanged. - **Any other model:** translated to the chat-completions shape — `system` → a system message, `tool_use` → `tool_calls`, `tool_result` → `tool` messages, `tools[].input_schema` → `function.parameters`, `tool_choice` `any` → `required`, `stop_sequences` → `stop`, `metadata.user_id` → `user`. Blocks without a neutral form (thinking, server tools) are not sent. - **Response:** an Anthropic `message` (`content` text/`tool_use` blocks, `stop_reason`, `usage` with `input_tokens` excluding cache reads/writes). With `stream:true` the SSE sequence is `message_start`, `content_block_start` / `content_block_delta` (`text_delta`, `input_json_delta`) / `content_block_stop`, `message_delta` (stop reason + usage), `message_stop`; a failure after the first byte ends in an `error` event. - **Errors:** `{"type":"error","error":{"type":"<anthropic type>","message":"…"},"request_id":"…"}` with the HTTP status of [Errors](/one-api/errors-limits) — `401` `authentication_error`, `402` `billing_error`, `429` `rate_limit_error`, `5xx` `api_error`, `504` `timeout_error`. ```bash curl -s https://gateway.plungeai.com/v1/messages \ -H "x-api-key: $PLUNGE_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{"model":"openai/gpt-4o-mini","max_tokens":64, "system":"Answer in one sentence.", "messages":[{"role":"user","content":"What is a Durable Object?"}]}' ``` Not yet live-verified (not deployed at time of writing; drill `tests/live/inference/anthropic-ingress.sh`). `api.plungeai.com/v1/messages` answers once the One API models proxy forwards the route. # Streaming Source: https://www.plungeai.com/docs/one-api/streaming Three surfaces stream over Server-Sent Events (SSE): the **models plane** (`/v1/chat/completions` with `stream:true`), the **workflows plane** (`/v1/workflows/execute-stream`), and the **agents plane** (`/v1/agents/:id/execute` with `stream:true`). The models plane speaks the OpenAI delta wire; the workflows plane speaks the CNL engine's named-event wire; the agents plane re-shapes an engine run into the OpenAI delta wire. All three send `Content-Type: text/event-stream` and frame events as `…\n\n`. ## 4.1 Models-plane SSE Code: `routes/chat-completions.ts` (stream branch) + `streaming/sse-translate.ts` + `streaming/usage-meter.ts`. **Wire format.** Each event is one `data: <json>\n\n` line whose payload is a `chat.completion.chunk`, terminated by a literal `data: [DONE]\n\n`. The gateway injects **no** comment lines on `/v1`; under the OpenRouter drop-in base `/api/v1` it sends `: OPENROUTER PROCESSING` until the first byte ([5.0](/one-api/openrouter-compat#50-drop-in-mode)). **Role primer.** Differs by provider shape: | Shape | First chunk | How | |---|---|---| | openai-shape (openai, groq, mistral, deepseek, xai, together, fireworks, cerebras, perplexity, openrouter, moonshot) | whatever the upstream sends (typically `delta:{role:"assistant"}`) | each upstream `data:` chunk is re-emitted by `shapeOpenAIStream` with the served `model` and `provider`; comment lines and non-JSON lines pass through unchanged. | | anthropic | `delta:{role:"assistant"}` injected by Ocean | `translateAnthropic` emits a primer in `start()`. | | google, cohere | `delta:{role:"assistant"}` injected by Ocean | emitted on the first upstream frame. | **Final `usage` chunk.** | Shape | Final `usage` present? | |---|---| | anthropic, google, cohere | **always** — the translator emits a final chunk carrying `finish_reason` + `usage` (anthropic adds `prompt_tokens_details` when cached). | | openai-shape | **only when you send `stream_options:{"include_usage":true}`** (or `usage:{"include":true}`) — the gateway itself asks OpenAI and DeepSeek for the usage chunk so billing never depends on you, and removes the usage-only chunk from the stream you receive when you did not ask. | Ocean meters tokens on every stream from the upstream's own usage chunk (a passive `usage-meter` sits between the upstream and the shaper; Groq's `x_groq.usage` is read too); what you receive still depends on `stream_options`. **`model` and `provider` on stream chunks.** Every chunk of every shape carries the served slug in `model` (also after a failover) and the canonical provider id in `provider`. **Response headers:** `content-type: text/event-stream`, `cache-control: no-cache, no-transform`, `x-accel-buffering: no`, `x-request-id`. **Failover vs mid-stream failure.** Failover between candidates is decided on the response **status**, before any SSE byte is read (`runAttempts`); once bytes are flowing there is no re-failover. A failure after that point travels in-band: the final chunk carries `error: { message, type: "upstream_error", code: "upstream_error", metadata: { provider_name, raw } }` and `finish_reason: "error"`, followed by `data: [DONE]` — for an Anthropic `error` event, a Gemini `error` object, or an upstream connection that dies mid-stream. An openai-shape upstream that sends its own `{error}` event passes it through as sent. **Cancellation / abort.** The client's `AbortSignal` is forwarded to the upstream fetch, so hanging up stops the provider. An aborted stream is billed for what it streamed — the completion tokens seen so far (a character estimate when the provider never sent usage) plus the prompt estimate — logged with `status 499` / `error_code client_abort`, and the hold is **settled** at that amount, not released (a trial org is logged but never debited). The */5 sweeper is only a backstop for a Worker that dies. There is no gateway-imposed stream idle timeout on this plane beyond the Workers runtime and the upstream's own. A streamed chat ends with `[DONE]` (live-verified 2026-10-04): ```bash curl -s "$API_BASE/v1/chat/completions" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "content-type: application/json" \ -d '{"model":"openai/gpt-4o-mini","stream":true,"messages":[{"role":"user","content":"Reply OK"}],"max_tokens":8}' ``` ## 4.2 Workflows-plane SSE Code: `routes/workflows.ts` + `lib/stream-relay.ts` (`workflowsEncoder`) + engine `orchestration/cnl-engine/context.ts`. Routes: `POST /v1/workflows/execute-stream` (inline YAML/JSON body), `POST /v1/workflows/:id/execute-stream` (saved workflow), and the legacy alias `POST /v1/cnl/execute-stream`. All require an `ozk_` key ([Authentication & keys](/one-api/authentication)). The relay **also drains** every provider sub-stream the run advertises and closes the execution row under `waitUntil`, so a client disconnect never aborts the run. **Wire format.** Engine frames pass through **verbatim** as `event: <name>\ndata: <json>\n\n`. Provider token deltas are injected as `event: token\ndata: {"task_id":"…","delta":"…"}\n\n`. Token frames for a task are flushed **before** that run's terminal frame (ordering guarantee). **Engine event vocabulary** (`CNLEventType`, `orchestration/cnl-engine/context.ts`): `request_received`, `workflow_loaded`, `workflow_started`, `task_dispatched`, `task_completed`, `task_error`, `task_skipped`, `streaming_started`, `streaming_completed`, `output_delivered`, `parallel_start`, `parallel_complete`, `sequential_start`, `sequential_complete`, `dynamic_start`, `dynamic_generated`, `dynamic_complete`, `batch_start`, `batch_item_start`, `batch_item_complete`, `batch_progress`, `batch_complete`, `debate_start`, `debate_round_complete`, `debate_judgment`, `debate_complete`, `condition_evaluated`, `workflow_yielded`, `workflow_result`, `workflow_completed`, `workflow_error`, `heartbeat`. Each `data` payload carries `workflow_id`, `execution_id`, `timestamp`, `type`, a nested `data` object, and `elapsed_ms`. The outcome frame is `workflow_completed` (carries `final_task_id`, `execution_summary`) or `workflow_error` (carries `error`); `workflow_result` is always the **last** frame, after the outcome — keep reading until it arrives. **Keepalive.** The engine emits its own `heartbeat` events; the relay forwards them verbatim and additionally touches the execution row's `updated_at` every ~60s so a long run is not swept mid-flight. This plane does **not** inject comment-line keepalives (that is the agents plane, [4.3](#43-agents-plane-openai-shaped-stream)). **Reading the final result.** After `workflow_result` (the last frame), fetch the stored output with `GET /v1/workflows/results/:workflowId/:taskId` (use the `final_task_id`) — see [Workflows](/one-api/workflows). While a run is in flight the results route returns `404 not_ready`. ## 4.3 Agents-plane OpenAI-shaped stream Code: `routes/agents.ts` (stream branch) + `lib/stream-relay.ts` (`openaiEncoder`). Requesting the stream: set `"stream": true` in the JSON body. This runs the same one-task engine envelope as the sync path but with `force_streaming`, and the `openaiEncoder` re-shapes the run into an OpenAI `chat.completion.chunk` stream — so an OpenAI SDK's streaming client works against an Ocean agent. (For the CNL-native event stream of the same agent, use the workflows plane instead.) **What the client sees** — only `data:` lines and comments, never engine `event:` lines (they are dropped by the encoder): - `id`: `chatcmpl-<executionId hex, 24 chars>` — derived from the execution id, stable across the stream. - `model`: the served model — a caller-pinned `provider/model`, else the provider's default from its `/health`, else `<provider>/default`. **Never** the agent id. - A primer chunk (`delta:{role:"assistant"}`) is emitted lazily on the first token. - Content chunks: `delta:{content:"…"}`. - Final chunk: `delta:{}` with `finish_reason` (`stop` or `length`) and, when the split is readable, a `usage` object; then `data: [DONE]\n\n`. - **Keepalive**: a comment line `: OCEAN PROCESSING\n\n` roughly every 15s while idle (so proxies don't time out a slow first token). **Error after the first byte.** Once SSE headers are committed, a run error is emitted as `data: {"error":{"message":…,"type":"server_error","code":"engine_error"}}` and the stream closes **without** `[DONE]` (matching OpenRouter's mid-stream-error behaviour). A rejection *before* the first byte is a normal JSON `502 engine_error` instead. **Headers:** `content-type: text/event-stream; charset=utf-8`, `cache-control: no-cache`, `x-accel-buffering: no`, `x-request-id`, and `X-Execution-Id` (the run id — reuse it to read the result later at `GET /v1/agents/results/:workflowId/:taskId`, [Agents](/one-api/agents)). ## 4.4 Verified examples An agent OpenAI-shaped stream — the primer, content deltas, a final `usage` chunk, and `[DONE]`: ```bash curl -s -N "$API_BASE/v1/agents/one-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt":"say hi in one word","stream":true,"max_tokens":16}' ``` A workflow event stream — engine frames pass through verbatim as named `event:` lines: ```bash curl -s -N "$API_BASE/v1/workflows/execute-stream" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workflow":"workflow:\n name: doc-stream\n tasks:\n - type: task\n id: t1\n agent: one-agent\n prompt: say hi"}' ``` # OpenRouter compatibility Source: https://www.plungeai.com/docs/one-api/openrouter-compat Point your OpenAI or OpenRouter client at `https://api.plungeai.com/v1`, swap the key for your `ozk_…` platform key, and `POST /v1/chat/completions` works with the same `provider/model` slugs and the same `models[]` fallback array — Ocean is an OpenAI-compatible router with OpenRouter-style multi-provider addressing. What it is **not** is a drop-in for OpenRouter's cost-accounting fields, its error shape, or its provider-routing request object. This chapter is the developer-facing matrix; the exhaustive live/static diff (every field probed, request ids logged) is the full comparison log at `orchestration/api-gateway/live-test-reports/2026-09-18-openrouter-compat/OPENROUTER-COMPAT-LOG.md`. ## 5.0 Drop-in mode Point an OpenRouter client at `https://api.plungeai.com/api/v1` and use your `ozk_` account key: `/api/v1/*` is the same models plane as `/v1/*` — same routing, guardrails, response cache, billing and metering — in OpenRouter's wire shape. Only the shape differs: | | `/api/v1` (OpenRouter shape) | `/v1` (OpenAI shape) | |---|---|---| | Error envelope | `{"error":{"code":401,"message":"…","metadata":{"error_type":"authentication","ocean_code":"invalid_api_key","ocean_type":"authentication_error","request_id":"…"}}}` — numeric `code`, provider detail in `metadata` | `{"error":{"message":"…","type":"authentication_error","code":"invalid_api_key","request_id":"…"}}` | | Completion `id` | `gen-…`, the generation id `GET /generation?id=` takes; also the `x-generation-id` header | `chatcmpl-…` | | `system_fingerprint` | always present (`null` when the provider sent none) | as the provider sent it | | Stream | `: OPENROUTER PROCESSING` keepalive until the first byte; the `usage` chunk is always sent and repeats `finish_reason` | `usage` chunk only with `stream_options.include_usage` | | `GET /models` | public; OpenRouter's item keys, `pricing` = USD-per-token strings, plus `links` and `total_count` | needs a key; Ocean's item shape ([3.8](/one-api/models#38-get-v1models)) | ```bash curl -s -X POST "$API_BASE/api/v1/chat/completions" -H "content-type: application/json" \ -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' ``` The drop-in base lists models without a key (live-verified 2026-10-04): ```bash curl -s -o /dev/null -w '%{http_code}\n' "$API_BASE/api/v1/models" ``` ## 5.1 Migration from OpenRouter | Concern | OpenRouter | Ocean | Change needed | |---|---|---|---| | Base URL | `https://openrouter.ai/api/v1` | `https://api.plungeai.com/v1` | change base URL | | Key | `sk-or-…` | `ozk_…` | new key ([Authentication & keys](/one-api/authentication)) | | Auth header | `Authorization: Bearer` | `Authorization: Bearer` | none | | Model slug | `provider/model` | `provider/model` | none (bare aliases also resolve, [3.7](/one-api/models#37-providers--slugs)) | | Fallback list | `models: [...]` | `models: [...]` | none — same field, same semantics ([3.3](/one-api/models#33-routing-models-sort-failover)) | | App attribution | `HTTP-Referer`, `X-Title` headers | ignored (no effect, no error) | drop them or leave them — they do nothing | | Cost in response | `usage.cost`, `cost_details` | `usage.cost`, `cost_details`, `is_byok` with `usage:{"include":true}` ([3.5](/one-api/models#35-response-object)) | none | | Provider routing | `provider:{...}` request object | not interpreted (see below) | use `sort` / `models` / [presets](/one-api/models#34-presets) | ## 5.2 Endpoints | OpenRouter path | Ocean | Notes | |---|---|---| | `POST /chat/completions` | **same** | field caveats in [5.3](#53-request-fields) | | `GET /models` | **same on `/api/v1`** · differs on `/v1` | public, OpenRouter's item shape under `/api/v1`; key-gated Ocean shape under `/v1` ([5.6](#56-models-list)) | | `POST /embeddings` | **differs** | openai, mistral, google and cohere models; no `/embeddings/models` list route ([3.9](/one-api/models#39-embeddings)) | | `POST /completions` (legacy text) | **same** | `prompt` must be one string; served on `/v1` and `/api/v1` ([3.1](/one-api/models#31-endpoints)) | | `GET /generation` (cost lookup) | **same** | `GET /generation?id=<gen-… id>` ([3.15](/one-api/models#315-account--generation-routes)); traces stay at `GET /v1/traces/:id` ([Traces](/one-api/traces-observability)) | | `GET /credits`, `/key` | **same** | ([3.15](/one-api/models#315-account--generation-routes)); `/auth/key` answers too. Key management (`/keys`, `POST /auth/keys`) stays in the dashboard | | `GET /models/{author}/{slug}/endpoints` | **same** | public under `/api/v1` ([3.15](/one-api/models#315-account--generation-routes)) | | `/images`, `/audio/*` | **missing** | not offered | | `/v1/agents`, `/v1/workflows`, `/v1/tools`, `/v1/mcp`, `/v1/discovery`, `/v1/traces` | **Ocean-only** | the execution planes ([5.9](#59-ocean-only-capabilities-you-gain)) | ## 5.3 Request fields Full per-provider-shape table is in [3.2](/one-api/models#32-request-fields). Relative to OpenRouter: | Field | OpenRouter | Ocean | |---|---|---| | `model`, `models`, `messages`, `stream`, `stream_options`, `temperature`, `top_p`, `max_tokens`, `stop`, `tools`, `tool_choice`, `response_format`, `seed`, penalties, `logit_bias`, `logprobs`, `top_k`, `min_p`, `top_a`, `reasoning`, `reasoning_effort` | supported | **openai-shape: forwarded** after one field split (OpenAI itself never receives `top_k`, `min_p`, `top_a`; `reasoning.effort` is sent as `reasoning_effort`). On anthropic/google/cohere some are dropped in translation and named in `x-ocean-dropped-params` — see [3.2](/one-api/models#32-request-fields). | | `sort` | not a request field | **Ocean-only** — `price`/`latency`/`throughput` | | `provider` (routing object) | supported | **ignored** — accepted and removed before the provider; use `models` / `sort` | | `route:"fallback"` | supported | **ignored** — accepted and removed; `models[]` always fails over in order | | `transforms`, `plugins` | supported | **ignored** — accepted and removed before the provider | | `prompt_cache_key`, `prompt_cache_options`, `cache_control` | supported | openai-shape passthrough; anthropic honours a `cache_control` marker on a system text part ([3.5](/one-api/models#35-response-object)), otherwise ignored | | `usage:{include:true}` | opt-in cost accounting | **supported** — adds `usage.cost`, `cost_details`, `is_byok` ([3.5](/one-api/models#35-response-object)); Ocean meters either way | ## 5.4 Response fields | Field | OpenRouter | Ocean | |---|---|---| | `id` | `gen-…` | `gen-…` under `/api/v1`; `chatcmpl-…` under `/v1` | | `object`, `created`, `model`, `choices`, `usage` | present | present (`model` is the honest served slug, [3.3](/one-api/models#33-routing-models-sort-failover)) | | `provider` (top-level) | present | present on every response and stream chunk | | `openrouter_metadata`, `system_fingerprint`, `service_tier` | present | `openrouter_metadata`, `service_tier` **absent**; `system_fingerprint` always under `/api/v1` (`null` when the provider sent none), as the provider sent it under `/v1` | | `choices[].native_finish_reason` | present | present on every choice | | `usage.cost`, `usage.cost_details`, `usage.is_byok` | present | present with `usage:{"include":true}` ([3.5](/one-api/models#35-response-object)); the non-stream price is also the `x-ocean-cost-usd` header | | `usage.prompt_tokens_details.cached_tokens` | present | present; Ocean also adds `cache_write_tokens` (Ocean-only) | | error-in-choice (mid-stream) | present | matched on the agent plane (error frame, no `[DONE]`) | ## 5.5 Streaming | Aspect | OpenRouter | Ocean | |---|---|---| | Wire | `chat.completion.chunk` + `[DONE]` | same ([4](/one-api/streaming)) | | Keepalive comment | `: OPENROUTER PROCESSING` | **same on `/api/v1`** (until the first byte); none on `/v1`; the [agents plane](/one-api/streaming#43-agents-plane-openai-shaped-stream) sends `: OCEAN PROCESSING` | | Final `usage` chunk | included (with `cost`) | **`/api/v1`: always**, repeating `finish_reason`; `/v1`: anthropic/google/cohere always, openai-shape only with `stream_options.include_usage` | | Mid-stream error | error object, close without `[DONE]` | matched on the agents plane; money-plane openai passthrough relays the upstream's own bytes | ## 5.6 Models list OpenRouter `GET /models` returns `data[].{id, canonical_slug, name, created, description, context_length, architecture, pricing:{prompt,completion}, top_provider, supported_parameters, …}` where `pricing` is **USD-per-token strings**. Ocean returns `data[].{id, object, created, owned_by, context_length, max_output, pricing:{in,out}, capabilities:{streaming,tools,vision,embedding,fim}}` where `pricing.in`/`.out` are **USD-per-1M numbers** ([3.8](/one-api/models#38-get-v1models)). `/v1/models` items also carry `name`, `canonical_slug`, `architecture`, `top_provider`, `supported_parameters`, `per_request_limits` and the per-token price as `pricing_per_token`; only `pricing` itself keeps the per-1M numbers. Under `/api/v1/models` the list is OpenRouter's: public (no key), `data[]` items carry `id, canonical_slug, name, created, description, context_length, architecture, pricing` (USD-per-token strings), `top_provider, per_request_limits, supported_parameters, default_parameters, links, supported_voices`, and the envelope adds `links` and `total_count`. ## 5.7 Errors OpenRouter's envelope is `{"error":{"message":"…","code":401,"metadata":{…}}}` — a **numeric** `code`, an optional `metadata`, no `type`. Ocean's money-plane envelope is `{"error":{"message":"…","type":"authentication_error","code":"invalid_api_key","request_id":"…"}}` — a **string** `code`, a `type`, a `request_id`, no `metadata`. The execution planes use a third shape (`{"error":{"code":"…","message":"…"}}` + `X-Error-Code` header). Full tables: [Errors, limits & headers](/one-api/errors-limits). ```bash curl -s "$API_BASE/v1/chat/completions" -H "content-type: application/json" \ -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":8}' ``` The `"code"` above is the string `invalid_api_key`, not the integer `401` an OpenRouter SDK expects. Under `/api/v1` the same request answers `"code":401` with `metadata.error_type` in OpenRouter's vocabulary (`authentication`, `payment_required`, `rate_limit_exceeded`, …) and the Ocean code in `metadata.ocean_code` ([5.0](#50-drop-in-mode)). ## 5.8 What breaks an OpenRouter SDK user today Every item below describes `/v1`. Under `/api/v1` ([5.0](#50-drop-in-mode)) the error envelope, `gen-` ids, keepalive, usage chunk and `/models` behave as OpenRouter's. - **`error.code` is a string**, not a number — code that branches on `err.error.code === 401` never matches (compare `err.status` / the HTTP status instead). - **Cost fields are opt-in** — `usage.cost`, `usage.cost_details`, `usage.is_byok` appear with `usage:{"include":true}` ([3.5](/one-api/models#35-response-object)); without it, read `x-ocean-cost-usd` or `GET /v1/generation` ([3.15](/one-api/models#315-account--generation-routes)). - **No `openrouter_metadata`** in the response; `provider` and `native_finish_reason` are present. - **OpenRouter-only request fields are not honoured** — `provider`, `route`, `transforms`, `plugins` are accepted and removed before the provider, with no effect. Replace with `models` + `sort` + [presets](/one-api/models#34-presets). - **No `: OPENROUTER PROCESSING` keepalive** on the money-plane stream; openai-shape streams carry a final `usage` chunk only if you pass `stream_options.include_usage`. - **`/models` shape differs** — needs a key, and `pricing` is per-1M numbers (the per-token strings are in `pricing_per_token`). - **`HTTP-Referer` / `X-Title` are ignored** — no app rankings / attribution. - **No `/images`, `/audio`, `/responses`, `/messages`, `/providers`** endpoints; `/generation`, `/credits`, `/key` ([3.15](/one-api/models#315-account--generation-routes)) and `/completions` ([3.1](/one-api/models#31-endpoints)) are served. - **BYOK is dashboard-configured**, not a per-request `provider` object ([3.12](/one-api/models#312-byok-or-billed)). ## 5.9 Ocean-only capabilities you gain - **Execution planes** an OpenRouter key cannot reach: [Tools](/one-api/tools), [Agents](/one-api/agents), [Workflows](/one-api/workflows), [MCP](/one-api/mcp), [Discovery](/one-api/discovery). - **`sort`** — route candidates by `price`, `latency`, or `throughput` ([3.3](/one-api/models#33-routing-models-sort-failover)). - **`@preset/<slug>`** — server-stored routing + param bundles ([3.4](/one-api/models#34-presets)). - **Guardrails** — org/key spend caps, model/provider allow-lists, content filter, `require_byok` ([3.10](/one-api/models#310-guardrails)). - **Exact-match response cache** — `$0` hits with an `x-cache` header ([3.11](/one-api/models#311-response-cache)). - **Traces & honest billing** — `GET /v1/traces/:id`, honest served-model reporting after failover ([Traces](/one-api/traces-observability)). # Tools plane Source: https://www.plungeai.com/docs/one-api/tools A **tool agent** is any active registry agent with a typed invocation contract. The tools plane is the shortest path from "I want Brave search" to a result: three routes, one security check, one execution path. Every call becomes a one-task workflow on the engine, so it lands in your execution history and carries an `x-request-id` you can trace ([Traces](/one-api/traces-observability)). ``` Discover ──► GET /v1/tools who is callable (the active catalog) Inspect ──► GET /v1/tools/:id the contract: operations, params, examples Execute ──► POST /v1/tools/:id/execute contract check → trust fence → engine → result ``` Auth: `Authorization: Bearer $PLUNGE_API_KEY` (an `ozk_` key — see [Authentication & keys](/one-api/authentication)). ## 6.1 List the catalog — `GET /v1/tools` Returns the active registry agents as summary cards. Paging: `limit` (default 50, max 100) and `offset`. The response `count` is the size of the page you received, not the catalog total — page until a short page comes back. ```bash curl -s "https://api.plungeai.com/v1/tools?limit=100" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` Response shape: `{ "tools": [ { "id", "type": "agent", "name", "description", "category", "tags": [...], "status": "active", "credential": {...}, ... } ], "count" }`. `credential` says what the agent runs on: `{ "tier": "platform" }` (our keys — nothing to set up) or `{ "tier": "connection", "provider", "method": "oauth" | "api_key" | "wallet" }` (the caller's own account at that provider — see [Connectors](/one-api/connectors)). Tags worth reading: `gated:<operation>` marks operations the trust fence will refuse unattended (see 6.4), `status:active` is the only status this route lists. On 2026-09-18 the catalog had **92 active tool agents in 14 categories** (`GET /v1/agents/categories` gives the live breakdown; the same query backs `GET /v1/agents`, so the two lists are identical — the difference is how you call them). ## 6.2 Inspect the contract — `GET /v1/tools/:id` ```bash curl -s "https://api.plungeai.com/v1/tools/brave-agent" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` | Field | Meaning | |---|---| | `agent_id`, `name`, `description` | the card identity | | `operations[]` | `{ name, description?, required_params? }` — the verbs the agent accepts as `operation`; an agent with no operations takes a `prompt` (or its `soleRequiredParam`) | | `inputSchema` | JSON Schema (`type: object`, `properties`, `required`, `additionalProperties`) for `params`; per-operation requireds live on `operations[].required_params` | | `examples[]` | `{ title, cnl }` — copy-paste CNL task snippets from the card's demonstrations | | `output_type` | prose description of what comes back | | `credential` | `{ tier: "platform" }` or `{ tier: "connection", provider, method }` — the same value the list carries; `GET /v1/tools/stripe-agent` answers `{ "tier": "connection", "provider": "stripe", "method": "api_key" }` | | `soleRequiredParam` | present when exactly one string param is required — a bare `prompt` is mapped onto it | | `credentials` | filled by surfaces that know the user's connections; absent here | A connection-tier agent says so in the same card: ```bash curl -s "https://api.plungeai.com/v1/tools/stripe-agent" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` Unknown id → `404 unknown_tool` (also in the `X-Error-Code` header): ```bash curl -s -o /dev/null -w '%{http_code} %header{x-error-code}\n' \ "https://api.plungeai.com/v1/tools/no-such-agent" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ## 6.3 Execute — `POST /v1/tools/:id/execute` Body (JSON by default; the same fields as YAML with `Content-Type: text/yaml`). The tool's fields may be sent bare at the root, exactly as the agent's README documents them (`{"expression": "7*6"}`); `params:` is still accepted and wins on a name clash. | Field | Type | Notes | |---|---|---| | `operation` | string | one of `operations[].name`; omit for prompt-only agents | | *the tool's fields* | per the contract | bare at the root (`query`, `count`, `expression`, …); validated against the contract | | `params` | object | the same fields wrapped; merged over the bare ones. Names that collide with task-envelope fields (`agent`, `id`, `type`, …) are `reserved` and rejected either way | | `prompt` | string | free-text input; satisfies missing requireds on prompt-capable agents | | `format` | `json` · `yaml` · `markdown` · `text` | response negotiation; `Accept` and the request `Content-Type` also count (default JSON, YAML in → YAML out) | Order of evaluation, from `orchestration/api-gateway/routes/tools.ts`: 1. **Contract validation.** `reserved` names, or missing requireds on a prompt-less call → `422 invalid_params` with `{ missing, reserved, warnings, contract }` — the contract is echoed so you can fix the call without a second request. 2. **The trust fence** (`checkAgentCall`, mode `unattended`) → `403 refused` or `409 approval_required` — see 6.4. Nothing is executed and no execution row is written. 3. **Execution.** A `workflow_executions` row opens under your user (`execution_id` = the request's trace id), the engine runs the one-task wrapper, the row closes with the result. Success (`200`): ```json { "ok": true, "content": "…markdown or text…", "outcome": "ok", "execution_id": "…", "request_id": "…" } ``` ```bash curl -s -X POST "https://api.plungeai.com/v1/tools/brave-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \ -d '{"prompt":"cloudflare workers pricing","count":3}' ``` Bare fields, YAML in → YAML out (the Content-Type mirror): ```bash curl -s -X POST "https://api.plungeai.com/v1/tools/calculator-tool-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary $'expression: "7*6"\n' ``` Operation form (the weather agent takes `location`; here the contract says what is missing): ```bash curl -s -w '\n%{http_code}\n' -X POST "https://api.plungeai.com/v1/tools/weather-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \ -d '{"operation":"current","params":{}}' ``` ## 6.4 Outcomes and the trust fence An agent that cannot do the job answers with a structured **outcome**, not prose (`core/core-base/agent-outcome.ts`). The tools plane maps outcomes onto HTTP (`routes/_util.ts` `outcomeStatus`): | `outcome` | HTTP | `error.code` | What to do | |---|---|---|---| | `ok` | 200 | — | read `content` | | `needs_input` | 422 | `invalid_params` | fix `params` — `error.detail` and the echoed `contract` say how | | `needs_connection` | 424 | `connection_required` | the account behind the call has not connected this provider — `error.connect` names the way in, see [Connectors](/one-api/connectors) | | `needs_api_key` | 424 | `credential_required` | the agent needs a key the account has not stored — `error.connect` names the way in | | `unavailable` | 503 | `agent_unavailable` | upstream/provider down — retry later or pick another agent | | `error` | 502 | `execution_failed` | the agent failed; `error.message` carries the agent's own text | The error body is `{ "error": { "code", "message", "outcome", "detail"?, "content"?, "contract"?, "connect"? } }` plus an `X-Error-Code` header. On a `424`, `connect` is `{ "provider", "method", "link": "POST /v1/connections/<provider>/link" }`; the Markdown, YAML and text wires print it as a `connect:` line under the execution footer. **The trust fence** (`core/core-base/call-agent.ts`) runs before every execution and cannot be turned off from the API: - **Always-gated operations** — `buy`, `purchase`, `fetch_paid`, `send`, `transfer`, `pay`, `send_payment`, `withdraw`, `shop` — plus any `gated:<op>` tag on the card. On this unattended surface they return `403 refused` (a human-in-the-loop surface such as Studio or a harness mission with an explicit allow-list returns `409 approval_required` instead). - **Guarded categories** — agents in the `payment` and `blockchain` categories are refused unattended unless a mission's `allowed_agents` names them. - **Not active** — a card that is not `status:active` is `403 agent_not_active`; an unknown id is `404 unknown_tool`. ```bash curl -s -w '\n%{http_code}\n' -X POST "https://api.plungeai.com/v1/tools/resend/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \ -d '{"operation":"send","params":{"from":"hello@example.com","to":"nobody@example.com","subject":"x","text":"x"}}' ``` A refusal writes no execution row — a denial is not a run. ## 6.5 The catalog by category (live 2026-09-18) Ids are exact; `GET /v1/tools/<id>` is the source of truth for operations and for `credential`. **Credential** is that field: `platform` runs on our keys; `connection` needs the caller's own account at the named provider, connected once over the API (chapter 7). A card whose agent also needs an account that is not yet in the machine-readable table (`core/core-base/credential-tiers.ts`) reads `platform`; those are named in the row. | Category | Agents | Credential | |---|---|---| | **search** (14) | `brave-agent`, `exa-agent`, `firecrawl-agent`, `tavily-agent`, `serper-agent`, `serpapi-agent`, `crawl4ai-agent`, `browserbase`, `arxiv-search`, `wikipedia-search`, `newsapi-get-articles`, `nyt-api`, `product-hunt-agent`, `branding-agent` | `platform` | | **tools** (23) | `calculator-tool-agent`, `markitdown`, `weather-agent`, `wolfram-alpha-full-results`, `data-analyst`, `code-reviewer`, `technical-writer`, `research-analyst`, `research-team`, `learn`, `pdf-worker`, `pptx-worker`, `xlsx-worker`, `ms-office-worker`, `report-worker`, `render-worker`, `cloud-browser-agent`, `gemini-file-search`, `google-docs-export-worker`, `n8n-converter`, `concierge`, `agentic-agent`, `universal-agent` | `platform` (`google-docs-export-worker`: `connection` · `google`) | | **ai** (3) | `llm-agent`, `vision-agent`, `skill-agent` | `platform` | | **google-workspace** (25) | `google-gmail`, `google-calendar`, `google-drive`, `google-docs`, `google-sheets`, `google-slides`, `google-forms`, `google-tasks`, `google-contacts`, `google-chat`, `google-meet`, `google-keep`, `google-classroom`, `google-admin`, `google-vault`, `google-analytics`, `google-search-console`, `google-ads`, `google-merchant-center`, `google-business-profile`, `google-books`, `google-youtube`, `google-translate`, `cloud-natural-language`, `google-workspace-assistant` | `connection` · `google` · OAuth (`google-books`, `google-translate`, `cloud-natural-language`: `platform`) | | **microsoft-365** (12) | `microsoft-outlook-mail`, `microsoft-outlook-calendar`, `microsoft-outlook-contacts`, `microsoft-onedrive`, `microsoft-sharepoint`, `microsoft-teams`, `microsoft-todo`, `microsoft-planner`, `microsoft-onenote`, `microsoft-excel`, `microsoft-word`, `microsoft-powerpoint` | `connection` · `azure` · OAuth | | **coding-agents** (1) | `gitlab-agent` | `connection` · `gitlab` · OAuth (a personal access token works too) | | **communication** (2) | `resend`, `cloudflare-email-agent` | `platform` — `send`/`send_batch` gated | | **media** (2) | `image-gen-agent`, `video-gen-agent` (`generate_video` gated) | `platform` | | **financials** (1) | `sec-agent` | `platform` | | **entertainment** (2) | `sports-schedule-agent`, `tv-listings-agent` | `platform` | | **ecommerce-agents** (2) | `amazon-shopping-agent`, `shopping-zinc` (`buy` gated) | `platform` — `amazon-shopping-agent` also needs an Amazon retailer login, made in Studio | | **payment** (2) | `x402-wallet-agent` (`fetch_paid` gated), `crossmint-agent` | `x402-wallet-agent`: `connection` · `x402-wallet` · wallet; `crossmint-agent`: `platform` — guarded category | | **blockchain** (2) | `circle-agent`, `circle-wallet-agent` | `platform` — the Circle wallet is connected in Studio; guarded category | | **shopping** (1) | `x402-broker` (`execute` gated) | `platform` — needs a wallet connected in Studio | ## 6.6 Using tools from your own LLM loop The tools plane is designed to sit behind any function-calling model — no SDK required: 1. `GET /v1/tools/:id` for the agents you want; turn each contract into a function definition (`name` = agent id, `parameters` = `inputSchema`, one function per `operations[].name` if you want the model to pick operations). 2. Send the definitions to your model as `tools` (any provider — the [models plane](/one-api/models) works too). 3. When the model returns a tool call, `POST /v1/tools/<id>/execute` with `{ "operation", "params" }`. 4. Feed `content` back as the tool result. On `422` feed back `error.message` + `error.detail` — the model fixes its own params. On `403 refused` tell the user a human has to approve that action. Every hop carries `x-request-id`; keep it in your logs and `GET /v1/traces/:id` shows the run. For a server-side loop that already does all of this (discover → inspect → execute with an approval gate), call the `concierge` agent on the [agents plane](/one-api/agents). # Connectors & connected accounts Source: https://www.plungeai.com/docs/one-api/connectors A **connector** is a third-party integration a user connects **once** — Gmail, Microsoft 365, GitHub, Adobe, Slack, Stripe, Notion, … A **connected account** is that user's live credential row. Tool agents then act *as that user*: `google-gmail` reads the mailbox of whoever owns the `ozk_` key that made the call. Nothing about the credential ever appears in the API. Three things are easy to confuse: | Thing | What it is | Where | |---|---|---| | **Tool agent** (`google-gmail`) | code that calls a provider's API on behalf of a user | tools / agents planes | | **Connector** (`google`, `github`, …) | the OAuth app or API-key slot a user connects | Studio → Integrations | | **Connector proxy provider** (`brave-search`, `firecrawl`, …) | a metered pass-through to a provider's REST API with your own or the platform's key | `api.plungeai.com/v1/connect/…` (7.5) | ## 7.1 Two credential tiers | Tier | How the user connects | Examples | Stored as | |---|---|---|---| | **OAuth** | clicks *Continue with <Provider>* in Studio, approves at the provider, lands back connected | Google Workspace, Microsoft 365, Adobe, GitHub, GitLab, Slack, Salesforce, HubSpot, Bitbucket, Shopify | encrypted access + refresh token; refreshed server-side when `tokensExpire` | | **API key / PAT** | pastes a key in the connector dialog | Anthropic, OpenAI, Stripe, Notion (internal token), GitLab PAT, Linear, Jira, Datadog, … | encrypted key; **verified live before storage** (`core/core-secrets/api-key-verify.ts` probes the provider — a rejected key is never saved) | Both land in `user_credentials` on the compliance database, AES-256-GCM encrypted with per-scope keys; every read is audited (`core/core-secrets/CLAUDE.md`). Agents never receive raw tokens from callers — the platform injects them at run time, which is why `params` names such as `api_key` or `token` are stripped by contract validation. ## 7.2 How a connected account flows into an API call ``` your app ──ozk_ key──► One API ──user_id──► engine ──► google-gmail agent │ resolveApiKey(google, user) ▼ user_credentials row? yes → call Gmail as the user no → outcome: needs_connection ``` Without a connection the agent answers with a structured refusal (`success: false`, `outcome: needs_connection`) and the tools plane returns `424 connection_required`: ```json { "error": { "code": "connection_required", "message": "Connect your Google account in Studio → Integrations to use google-gmail", "outcome": "needs_connection", "connect": { "provider": "google", "method": "oauth", "link": "POST /v1/connections/google/link" } } } ``` `connect` is the way in: the connector (`provider`), how it connects (`method`: `oauth`, `api_key` or `wallet`) and the route that starts it ([7.3](#73-how-your-testers-connect-an-account)). Every `424` from the tools, agents, workflows and Agent Call routes carries it when the agent is connection-tier; the Markdown and text wires print it as `connect: POST /v1/connections/<provider>/link` under the execution footer. `credential_required` (`needs_api_key`) carries it the same way. The same run **with** a connection returns `200 { ok: true, content }` — live example from 2026-09-18 with a Microsoft 365 connection on the calling account (`microsoft-todo`, operation `list_lists`, request id `00000000-0000-4000-8000-000000000003`). Which agents need what: the `credential` field on every card (`GET /v1/tools/<id>`, the lists, `GET /v1/discovery/search`) — `{ "tier": "platform" }` or `{ "tier": "connection", "provider", "method" }`; the **Credential** column in [the tool catalog](/one-api/tools#65-the-catalog-by-category-live-2026-09-18) is that field. The rule of thumb: `google-*` needs the `google` connector (except the key-based tool APIs `google-books`, `google-translate`, `google-places-agent`, `google-cloud-iot-agent`), `microsoft-*` needs Microsoft 365 (connector id `azure`), `adobe-*` Adobe (not `adobe-pdf-services`), `gitlab-agent` GitLab, `stripe-agent` a Stripe key; search/scrape/document tools run on platform keys. ## 7.3 How your testers connect an account Connecting is an API call. The `ozk_` key that makes the call owns the connection, and no Studio login is needed: | | Call | Answer | |---|---|---| | **Bring a key** | `POST /v1/connections/:provider` · `{ "api_key", "api_secret"?, "on_behalf_of"? }` | `201 { provider, on_behalf_of, status: "connected" }`; `400 credential_rejected` when the provider refuses the key (providers with a live probe — GitLab today — are checked before anything is stored); `404 unknown_provider` | | **Send a hosted link** | `POST /v1/connections/:provider/link` · `{ "on_behalf_of"?, "return_url"? }` | `201 { url, expires_at }` | | **List** | `GET /v1/connections?on_behalf_of=` | `200 { connections: [ { provider, on_behalf_of, method, status, connected_at } ] }` — status only; no token, key or hint is ever returned | | **Disconnect** | `DELETE /v1/connections/:provider?on_behalf_of=` | `204`; `404 connection_not_found` | The hosted link is `https://studio.plungeai.com/connect/<token>`: signed, **single-use**, valid **15 minutes**, and it works without a Studio session — send it to the person who owns the account. The page opens with who is asking — your Studio name, else your email — and says that your agents will act on the account on their behalf until they disconnect it. For an OAuth provider the page is *Continue with <Provider>* → approve at the provider → back to your `return_url` (an `https` URL you gave, with `?connected=<provider>` added) or, without one, a confirmation page. Where Studio has no OAuth app for the provider the page is a key form: the pasted key is checked and stored the same way as `POST /v1/connections/:provider`. A tampered or expired link is `404`, a used one `410`. ```bash curl -s -X POST "https://api.plungeai.com/v1/connections/google/link" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \ -d '{"return_url":"https://example.com/done"}' ``` **Your customers (`on_behalf_of`).** Name a customer with any id you own — 1 to 128 characters of `A-Z a-z 0-9 _ . -` (no `..`, no `:`) — and the connection is that customer's, not yours: it is stored under your key and your customer id, so two developers who use the same id never collide, `GET /v1/connections` shows only the account you ask for (yours, or the one `on_behalf_of` names) and nobody can reach a customer of another key. An empty `on_behalf_of` is refused, never read as your own account. To run an agent as a customer, send the same id on the call: `POST /v1/execute` with `on_behalf_of: cust_42` next to `agent:`. A connection-tier agent then uses the account connected under that id (and answers `424` with `connect` when that customer has none) — never yours; the run, its billing and a saved agent stay yours, and the result is returned to you as usual. It is a wire key, like `format`: it never reaches the agent as a field. It is accepted on `POST /v1/execute` only: the per-id doors (`/v1/tools/{id}/execute`, `/v1/agents/{id}/execute`, `/v1/workflows/{id}/execute` and their stream variants) answer `400 invalid_body` rather than quietly running as you. Provider ids are the connector ids of [7.4](#74-the-connector-catalog) — the `credential.provider` of a card or the `connect.provider` of a `424` names the one to use (`google`, `azure` for Microsoft 365, `github`, `slack`, `stripe`, …). The same connection can be made in **Ocean Studio** (`https://studio.plungeai.com`), where a signed-in user connects their own account in a browser: 1. Studio → **Integrations** → **Browse connectors** → pick the provider. 2. OAuth connector: **Continue with <Provider>** → approve → back in Studio as *Connected*. API-key connector: paste the key → it is probed live → *Connected* (or an inline "rejected" error). 3. Verify with the API: run the agent with the same user's `ozk_` key — the `needs_connection` outcome disappears. The Studio endpoints behind those buttons (session cookie, not API key — listed so you know what happens, not for you to call): `GET /api/connections` (yours), `GET /api/connections/providers` (public: every OAuth provider and its scopes), `GET /api/auth/oauth/:provider/start`, `POST /api/connections/api-key` (`400` when the live probe rejects the key), `DELETE /api/connections/:provider`. Special flows exist for the Circle agent wallet (`/api/connections/circle-agent-wallet/init|verify`), retailer logins such as Amazon (`/api/connections/retailer/connect`) and QR/browser logins such as WhatsApp (`/api/connections/browser-session/*`) — those connect in Studio only. ```bash curl -s "https://studio.plungeai.com/api/connections/providers" ``` Redirect URIs and OAuth app registration are platform-side — testers never configure anything at the provider. ## 7.4 The connector catalog The registry lists **802 connector cards** (`GET /v1/discovery/search?kind=connectors&limit=100`, page with `offset` until `total`; live 2026-09-29). Every card carries `connector.auth` — never null: | `connector.auth` | Meaning | Count | |---|---|---| | `oauth_static` | a platform-registered OAuth app; user clicks Continue | 46 | | `api_key` | user pastes a key/PAT | 83 | | `oauth_dcr` | an MCP server the user connects with Dynamic Client Registration (see [MCP plane](/one-api/mcp)) | 519 | | `none` | platform-key or public provider — nothing to connect. Also what a card reads when its source declared no auth (57 of the 154 today: 35 community mirrors and 22 legacy tool cards) | 154 | ```bash curl -s "https://api.plungeai.com/v1/discovery/search?kind=connectors&limit=3" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ```bash curl -s "https://api.plungeai.com/v1/discovery/cards/connector/slack" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` **OAuth apps registered on the platform today** (client id wired in the Studio worker, `wrangler-templates/template.wrangler.ocean.jsonc` ↔ `core/core-secrets/oauth-providers.ts`): `google` (Workspace: Drive, Gmail, Calendar, Contacts, Tasks, Docs, Sheets, Slides, Analytics, Search Console, Merchant Center, Business Profile, YouTube; `google-youtube` also connects YouTube on its own), `azure` (Microsoft 365: OneDrive, Outlook, Calendar, Teams, SharePoint, To Do, Planner, OneNote, Office files), `adobe` (Creative Cloud, Express, Photoshop, Lightroom, Stock, Experience Platform, Journey Optimizer), `github`, `gitlab`, `bitbucket`, `slack`, `salesforce`, `hubspot`, `shopify`. Provider entries also exist for `sonos`, `spotify`, `tesla`, `facebook`, `linkedin`, `twitter`, `reddit`, `tiktok`, `pinterest`, `snapchat`, `intuit`, `square`, `zoom`, `sentry`, `notion` — their cards show in Studio, but the OAuth app is not registered yet, so *Continue with …* bounces with `oauth_not_configured`; connect those with an API key where the card offers one. By category (ids as the registry spells them): | Group | Connectors | |---|---| | **AI & ML** | `openai`, `anthropic`, `huggingface`, `ibm` | | **Google** | `google` (Workspace umbrella), `google-drive`, `google-gmail`, `google-calendar`, `google-contacts`, `google-tasks`, `google-youtube` | | **Microsoft** | `azure` (Microsoft 365 umbrella), `microsoft-onedrive`, `microsoft-outlook-mail`, `microsoft-outlook-calendar`, `microsoft-teams`, `microsoft-sharepoint`, `microsoft-365` | | **Adobe** | `adobe`, `adobe-express`, `adobe-photoshop`, `adobe-lightroom`, `adobe-stock`, `adobe-cc-files`, `adobe-aep`, `adobe-ajo`, `adobe-workfront` | | **Developer & cloud** | `github`, `gitlab`, `bitbucket`, `postman`, `aws`, `cloudflare`, `vercel`, `netlify`, `supabase`, `databricks`, `snowflake` | | **Observability** | `sentry`, `datadog`, `grafana-mcp-server`, `honeycomb`, `incident-io`, `logrocket-mcp`, `jam` | | **Communication** | `slack`, `discord`, `telegram`, `twilio`, `sendgrid`, `mailchimp`, `resend`, `whatsapp`, `whatsapp-personal` | | **Productivity & PM** | `notion`, `airtable`, `asana`, `jira`, `linear`, `monday`, `clickup`, `trello`, `basecamp`, `teamwork`, `craft`, `mem`, `roam-research`, `ticktick`, … (28 productivity + 19 project-management cards) | | **CRM, sales & marketing** | `salesforce`, `hubspot`, `pipedrive`, `zoho`, `close`, `helpscout`, `intercom`, `attio`, `apollo-io`, `klaviyo`, `lemlist`, `outreach`, `salesloft`, `zoominfo`, … (49 marketing-and-sales cards) | | **Payments & finance** | `stripe`, `paypal`, `square`, `plaid`, `xero`, `circle`, `circle-agent-wallet`, `x402-wallet`, `intuit`, `intuit-quickbooks`, `intuit-payments`, `intuit-payroll`, `intuit-time-tracking`, `airwallex` | | **Data & analytics** | `amplitude`, `mixpanel`, `segment`, `looker`, `alpha-vantage` | | **Search & scraping (platform-key providers)** | `brave-search`, `exa`, `firecrawl`, `crawl4ai`, `browserbase`, `scrapingbee`, `serpapi`, `serper`, `perplexity-search`, `news-api`, `nyt`, `arxiv`, `wikipedia`, `product-hunt`, `weather`, `wolfram-alpha` | | **Social** | `facebook`, `instagram`, `threads`, `linkedin`, `twitter`, `reddit`, `pinterest`, `snapchat`, `tiktok`, `youtube`, `bereal`, `clubhouse` | | **Commerce & travel** | `shopify`, `amazon`, `target-app`, `shop`, `instacart`, `expedia`, `booking`, `tripadvisor`, `uber`, `viator`, `kiwi-com`, `amadeus`, … | | **Legal, security, research, lifestyle** | 13 legal, 15 security, 8 research and 11 lifestyle MCP-server connectors (`oauth_dcr` / `none`) | ## 7.5 The connector proxy — `/v1/connect/:provider/proxy/*` For providers you would otherwise call directly, the inference gateway offers a **metered pass-through** (`inference/gateway/src/routes/connector-proxy.ts`): one `ozk_` key, one bill, transparent bytes in both directions (streaming included, no body transformation). | | | |---|---| | **Key** | your `ozk_…` platform key (Dashboard → One API → Keys, `https://dashboard.plungeai.com`); a legacy `sk-conn-…` key is still accepted until it is revoked and replaced | | **Route** | `ANY https://api.plungeai.com/v1/connect/<provider_id>/proxy/<upstream path>` — the path after `/proxy/` is appended to the provider's `base_url` | | **Mode** (from the `connector_providers` rate card) | `byok` — your own upstream token connected for the org is used, you pay `byok_routing_fee`; `platform` — the platform's key is used, you pay `price_per_call`; `both` — your token if connected, else the platform's | | **No connection, provider is BYOK-only** | `402` "connection required" | | **Unknown or inactive provider** | `404 model_not_found` / `400 provider_unavailable` | | **Limits** | per-provider `rate_limit_rpm`; org spend caps and guardrails apply exactly as on the models plane | | **Auth failures** | `401 invalid_api_key`; a suspended or closed account is `401 Account suspended` | Example (one metered provider call, so it carries no `# verify` marker): ```bash curl "https://api.plungeai.com/v1/connect/brave-search/proxy/res/v1/web/search?q=cloudflare+workers" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` Each call is metered as a `connector_requests` row and appears in the Dashboard next to your inference spend. Prefer the tools plane when you want the agent's judgement (result formatting, pagination, outcome vocabulary); use the proxy when you want the provider's raw API with one key and one bill. # Discovery Source: https://www.plungeai.com/docs/one-api/discovery Discovery is the read-only catalog behind every other plane: agents, skills, plugins, MCP servers, connectors, models, providers and (once you have saved some) workflows. Four routes, all `ozk_`-authenticated, all backed by the registry service (`orchestration/api-gateway/routes/discovery.ts` → `orchestration/registry/`). ## 8.1 Search — `GET /v1/discovery/search` | Param | Values | Notes | |---|---|---| | `q` | text | natural-language or keyword query; with `q` the registry runs **hybrid** search (semantic + keyword), without it a plain listing | | `kind` | `agents` · `skills` · `plugins` · `mcp-servers` · `connectors` · `models` · `providers` · `workflows` · `experts` · `backgrounds` · `personas` (= `digital-twins`) | the plural catalog groups (`registry.ts` `KIND_GROUPS`) | | `type` | singular storage type (`agent`, `skill`, `mcp_server`, …) | alternative to `kind` | | `category` | e.g. `search`, `google-workspace` | exact card category | | `status` | `active` (default) | the public catalog shows callable capabilities only; other statuses need an explicit value | | `tier` | card tier tag | e.g. `verified` | | `mode` | `hybrid` (default with `q`) · `keyword` · `vector` | force one search leg | | `fields` | `summary` · `full` | `full` adds parameters, demonstrations and `prompt_format` | | `limit`, `offset` | ints | `limit` is capped by the registry; page with `offset` | | `include` | `quality` | attaches a `quality` object per agent card — `{ score, success_rate, runs }` or `null` when the evaluation service has no data (it was `null` for every card on 2026-09-18) | Response: `{ "cards": [...], "count": <cards in this page>, "total": <every match>, "searchMethod": "text" | "hybrid" | "vector", "query": { …the normalised query… } }`. `count` is the page size; `total` is every card the filters (and, for `q`, the keyword predicate) match, ignoring `limit` and `offset` — page with `offset` until you have seen `total`. For a `q` search the vector leg can add cards the keyword predicate did not match, so `total` is a floor, never smaller than `offset + count`. ```bash curl -s "https://api.plungeai.com/v1/discovery/search?q=web+search&kind=agents&limit=3" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ```bash curl -s "https://api.plungeai.com/v1/discovery/search?q=llm&kind=agents&limit=2&include=quality" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` Catalog sizes on 2026-09-18 (`limit=100`, first page): agents **92**, skills **12**, plugins **5**, providers **11**, mcp-servers **100+**, models **100+**, connectors **802** as of 2026-09-29 (9 pages of 100 — read `total`), workflows 0 for a fresh key (saved workflows are per user), experts / backgrounds / personas 0 (capability cards are created by users and missions — see [Skills, plugins & capability injection](/one-api/skills-plugins-capabilities)). ## 8.2 The card object Summary cards (`fields=summary`, the default for the agents/tools lists) carry: `id`, `type`, `name`, `version`, `description`, `category`, `best_for[]`, `not_for[]`, `tags[]`, `status`, `icon`, `color`, `created_at`, `updated_at`. Full cards add `parameters[]`, `demonstrations[]`, `prompt_format`, `output_type`, `output`. Connector cards add a `connector` object (`auth`, `provider_key`, `key_placeholder`, `help_text`); model cards add pricing and capability columns from the rate card. Tags encode the operational facts the planes act on: | Tag | Meaning | |---|---| | `status:active` | callable — the only status the tools/agents planes accept | | `gated:<op>` | the trust fence refuses `<op>` unattended (403) — [Tools plane](/one-api/tools) | | `tier:verified` | reviewed card | | `auth:oauth_static` · `auth:api_key` · `auth:oauth_dcr` | how a connector is connected — [Connectors](/one-api/connectors) | | `serves:agent` | a connector some tool agent uses | ## 8.3 Recommend — `POST /v1/discovery/recommend` Body `{ "type": "agent" | "skill" | … (plural kinds accepted as aliases), "task": "<what you want done>", "status"?: "<status>" }`. **Since 2026-09-25 recommend rides on the Jev resolve ([§8.4](#84-resolve--post-v1discoveryresolve)) and works again** — from 2026-09-18 it answered `502 upstream_error` for every request. `status` defaults to `active`. Response: `{ "recommended": "<card id>" | null, "card": { …the card… } | null, "score": <0–1>, "resolve": { …the full resolve decision… } }` — `recommended` is `null` and `score` is `0` when nothing fits; `score` is also `0` on the keyword fallback (no confidence). Only the types in the discovery index can be recommended — `agent`, `skill`, `connector`, `mcp_server`, `workflow`, `plugin`, `expert`, `digital_twin`; any other valid type (`model`, `provider`, `background`, `component`, `theme`, …) answers `400 type_not_discoverable` (legacy aliases such as `ai_agent` are normalised first), an unknown type `400 invalid_request`. ## 8.4 Resolve — `POST /v1/discovery/resolve` One step from a task to a call: which tool, which operation, what command, and how sure the platform is. The registry shortlists cards per kind (BM25 over id, name, description, `best_for` and tags), one decision request to the Jev model (`typesafe-provider`) picks a card per kind or none, and the answer carries that card's operation, its required parameters, an example CNL task and a calibrated confidence (`orchestration/registry/resolve.ts`). ```bash curl -s -X POST "https://api.plungeai.com/v1/discovery/resolve" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"task": "current weather conditions for Berlin", "kinds": ["agents"]}' ``` The body is whitelisted — any other field is dropped; you never pass an owner or a debug flag, so only public cards are considered. | Field | Values | Notes | |---|---|---| | `task` | text, required | what you want done, in plain language; missing or empty → `400 invalid_json` | | `kinds` | array of card types: `agent` · `workflow` · `connector` · `mcp_server` · `skill` · `plugin` · … | plural kinds (`agents`, `mcp-servers`, …) are accepted aliases; deduplicated, at most 8; default: `agent`, `workflow`, `connector`, `mcp_server`, `skill`, `plugin` | | `category` | e.g. `search` | exact card category | | `status` | `active` (default) · `ready` · `any` · a literal status | same default as search: callable capabilities only | Response fields: | Field | Meaning | |---|---| | `outcome` | `match` · `uncertain` · `none` — see below | | `tool` | the chosen card: `{ id, kind, type, name, description, category, status }`; absent on `none` | | `operation` | `{ name, purpose?, gated }` — absent when the card has several operations and none was decided; `gated: true` means the [trust fence](/one-api/tools) needs an approval | | `call` | `{ required_params[], example_cnl? }` — the example is a CNL task you can adapt ([workflows](/one-api/workflows)) | | `confidence` | 0–1, calibrated; `null` on the keyword fallback | | `alternatives[]` | up to 5 runners-up `{ id, kind, name, p }` | | `gate` | advisory signals from the same request: `acts_on_user_system`, `needs_external_service`, `prose_suffices` (0–1) and a suggested `handler` | | `method` | `all` (every card of each kind was shown to the model) · `bm25` (a keyword shortlist) · `cache` (a stored decision) · `lexical-fallback` (the decision model was unavailable) · `rate-limited` (a public caller exceeded the per-isolate Jev budget — the pick is a keyword match with `confidence: null`) | | `pass` | `1`, or `2` when a second, closer comparison of the top three was needed | | `mode` | `shadow` · `act` — how the platform's own agents use resolve (in `shadow` they log the decision and keep showing the plain search list); it does not change your answer | | `index_version`, `timings` | the catalog snapshot the decision used, and the server-side legs in ms | What each outcome means for a caller: - **`match`** (confidence ≥ 0.8) — call `tool.id` with `operation.name`; fill `call.required_params` (the card at [§8.5](#85-one-card--get-v1discoverycardstypeid) has every parameter). Ask your user first when `operation.gated` is `true`. - **`uncertain`** — the best guess is below 0.8, or `method` is `lexical-fallback` or `rate-limited` and the pick is a keyword match (`confidence: null`). Treat `tool` and the first `alternatives` as a shortlist: read their cards and choose, or ask your user. - **`none`** — no card in the catalog fits this task, or the task needs no tool at all (plain text answers it). Do not invent an id; answer without a tool or tell your user nothing fits. Decisions for the same task, kinds, status and category are cached (10 minutes in memory, 6 hours in KV) until the catalog snapshot changes. ## 8.5 One card — `GET /v1/discovery/cards/:type/:id` Returns the **full card as markdown with YAML frontmatter** (`Content-Type: text/markdown`) — the same document the platform's own agents read before calling a tool. `:type` is the singular storage type (`agent`, `skill`, `plugin`, `mcp_server`, `connector`, `model`, `provider`, `workflow`, `expert`, `background`, `digital_twin`); the plural `kind` spellings (`agents`, `mcp-servers`, …) are accepted as aliases. ```bash curl -s "https://api.plungeai.com/v1/discovery/cards/agent/brave-agent" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` Unknown card → `404 not_found`. A 4xx from the registry is normalised into the One API error envelope; a registry 5xx never reaches you raw (`502 upstream_error`). ## 8.6 From an id to a call | Card `type` | Use it as | |---|---| | `agent` | `GET /v1/tools/<id>` + `POST /v1/tools/<id>/execute` ([tools](/one-api/tools)); `POST /v1/agents/<id>/execute` ([agents](/one-api/agents)); `agent: <id>` in a CNL task ([workflows](/one-api/workflows)) | | `model` | `"model": "<id>"` on `POST /v1/chat/completions` — ids are `provider/model` slugs ([models](/one-api/models)); also `model:` + `provider:` on an agent-plane call | | `provider` | the `provider/` prefix of model slugs; `provider:` on agent-plane calls | | `skill` · `plugin` · `expert` · `background` · `digital_twin` | `skills: [<id>]`, `plugins: [<id>]`, `experts: [<id>]`, `backgrounds: [<id>]`, `persona: <id>` on a `type: harness` task ([capability injection](/one-api/skills-plugins-capabilities)) | | `mcp_server` | `POST /v1/mcp/runs` with the server id ([MCP plane](/one-api/mcp)); `mcp: [<id>]` on a harness mission | | `connector` | connect it in Studio → Integrations; then the agents that `serves:agent` it stop returning `needs_connection` ([connectors](/one-api/connectors)) | | `workflow` | `POST /v1/workflows/<id>/execute` ([workflows](/one-api/workflows)) | Search results are cached briefly on the platform side (KV + LRU, ~200 ms per uncached call); agents that discover at run time — `concierge`, the harness loop — hit this same route, so what you see is what they see. # Agents plane Source: https://www.plungeai.com/docs/one-api/agents The agents plane runs one registry agent per request and hands back its result. It is the execution plane you reach with an `ozk_` key (see [Authentication](/one-api/authentication)) at `POST /v1/agents/:id/execute`. Every call wraps exactly **one** minimal CNL task in the engine envelope, so the agents plane is the single-task front door; multi-task orchestration lives on the [Workflows plane](/one-api/workflows) and bounded autonomous missions on a `type: harness` task there (see [capability injection](/one-api/skills-plugins-capabilities)). Base URL is always `https://api.plungeai.com`. All examples below use `$PLUNGE_API_KEY` for an `ozk_…` key. Source: `orchestration/api-gateway/routes/agents.ts`. ## 9.1 Agent tiers An **agent** is a registry card whose id equals its Worker service name. There are ~90 active agents (`GET /v1/agents?limit=100` returned `count: 92`, live-verified 2026-09-19; the default page is 50 — follow `has_more`/`next_offset`). They fall into these tiers. Every id below is **status:active** in the registry unless the row says otherwise (`agents/agents/README.md`, `agents/agents/CORE-AGENTS-DECISION.md`). | Tier | id(s) | What it is | Use when | Call how | |---|---|---|---|---| | **Tool agent** | ~90 ids: `serper-agent`, `exa-agent`, `tavily-agent`, `sec-agent`, `serpapi-agent`, `markitdown`, `weather-agent`, … | a registry agent with a typed invocation contract (parameters/operations) | you need one bounded capability (search, documents, finance, a connector) | `POST /v1/agents/:id/execute` with a `prompt`; or the [Tools plane](/one-api/tools) `POST /v1/tools/:id/execute` with typed params; or an `agent:` task in a workflow | | **`one-agent`** — the agent runtime | `one-agent` | the one runtime, two modes chosen by the payload. **Call mode** (no loop command): a single LLM call — YAML in → provider RPC → result — with `persona` / `expert(s)` / `skill(s)` injection and `messages` threading. **Loop mode** (`max_iterations` / `maxTurns` or a mission): a bounded ReAct mission — tool fence, iteration cap, long-term memory, capability injection | any generation (pinned model/provider, shaped by a persona or skill) and multi-step autonomous work that calls tools/sub-agents | call mode: agents-plane execute; `model`/`provider`/`system`/`messages`/`persona`. Loop mode: a `type: harness` task with `agent: one-agent` on the [Workflows plane](/one-api/workflows) — the agents plane forwards no mission fields (see 9.3), so it runs call mode only | | **`concierge`** | `concierge` | interactive Discover→Inspect→Execute front-door over any agent, with a payment/approval gate | "do X" where the platform must find and call the right agent, with a money gate | its own approval contract (9.7) and the [MCP plane](/one-api/mcp) `plungeai_continue`; on the raw agents plane re-send with `approved: true` + `action_token`, or approve by prompt (`approve <token>`, 9.7) | | **`claude-managed-agent`** | deployed, **not** in the active list | Anthropic hosted Managed Agents runtime for autonomous cloud code execution | autonomous cloud code runs (higher infra overhead) | not exposed on the ozk agents plane today — live-verified 2026-09-18 it is absent from `GET /v1/agents`, so `/v1/agents/claude-managed-agent/execute` returns `404 unknown_agent` | | **Campaign kind** | Studio `kind: campaign` (no `campaign-agent` Worker) | a scheduled, ledger-driven workflow that works a task list in short runs | recurring bounded batch work | via the scheduler / MCP `plungeai_schedule` / Studio, not a callable agent id (`agents/agents/campaign-agent/README.md`) | > **Legacy ids.** `llm-agent`, `skill-agent`, `universal-agent` and `agentic-agent` are frozen > legacy ids: they stay callable until the switch-over, then re-point to `one-agent`. Write > `agent: one-agent` everywhere — a `type: harness` task with **no** `agent:` line still runs on the > legacy `harness-agent` loop today. The bare `harness-agent` binding is not a callable registry id > on the planes (live-verified: `404 unknown_agent`). ## 9.2 List agents & categories - `GET /v1/agents?limit=<1..100>&offset=<n>` → `{ agents: [...], count, limit, offset, has_more, next_offset }` — `count` is the page size (default limit 50); when `has_more` is true, fetch the next page at `offset=next_offset`. Each card carries `id`, a summary and `tags` (including `status:active`). - `GET /v1/agents/categories` → `{ categories: [{name, count}], count }`, aggregated from the active-agent summaries (there is no separate categories store). ```bash curl -s "https://api.plungeai.com/v1/agents?limit=5" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ## 9.3 Execute — body reference `POST /v1/agents/:id/execute`. JSON body (default) or an Ocean-format `text/yaml` body with the same fields. Every accepted field, from `routes/agents.ts`: | Field | Type | Status | Meaning | |---|---|---|---| | `prompt` | string | **supported** — required unless `messages` has a user turn | the per-run input. With no `prompt`/`input`, the last non-blank user message of `messages` is used; neither → `400 missing_prompt` | | `input` | string | supported | alias for `prompt` | | `sync` | boolean | supported (default `true`) | `false` → `202` pointer, poll the result route (9.5) | | `stream` | boolean | supported (default `false`) | `true` → an OpenAI-shaped SSE stream (9.6) | | `messages` | array | supported | prior chat turns `[{role, content}]`; **`prompt` is optional when `messages` has a user turn** (the last one is used). `one-agent` (call mode) sends the messages — not a separate prompt — to the model, so it answers from the whole conversation; any other agent receives `messages` as an input field and threads it only if it reads it. For a conversation that survives across calls use the [MCP plane](/one-api/mcp) (`session_id`, `plungeai_continue`) | | `system` | string | supported | system-prompt frame forwarded to the agent | | `model` | string | supported | pin a model; validated against the catalog → `422 unknown_model` if unknown/inactive | | `provider` | string | supported | pin a provider; normalised (`google`→`gemini`). Must travel **with** `model` — a pinned model without its provider routes to the agent default | | `temperature` | number | supported | sampling temperature | | `top_p` | number | supported | nucleus sampling | | `max_tokens` / `maxTokens` | number | supported | output-token budget (must be `> 0`) | | `reasoning_effort` | string | supported | reasoning-effort hint forwarded to the model | | `thinking_level` | string | supported | thinking-level hint forwarded to the model | | `persona` | string | supported | the **only** capability field forwarded on this plane | | `format` | `json`\|`yaml`\|`markdown`\|`text` | supported | response format (also `?format=` or `Accept`). Unknown value → `400 invalid_format` (`lib/format.ts`) | | `user_context` | object · optional · default: derived from your request's network location (time zone, approximate place) | supported | the caller's time zone, locale and location (`{time_zone, locale, place, latitude, longitude, accuracy_m, source}`, the `UserContext` schema); the agent uses it for local time and as the default schedule time zone | Fields not in this table (e.g. `goal`, `mission`, `allowed_tools`, `skills`, `experts`, `mcp`) are **ignored** on the agents plane — run those through a `type: harness` task on the [Workflows plane](/one-api/workflows). Two more fields are accepted only as a pair, the approve round-trip of [9.7](#97-the-concierge-approval-flow): `approved: true` with `action_token` (a string of 1-128 characters), plus an optional `allow_scope` (`once` | `run` | `always`, anything else is `once`). One without the other, or an `approved` that is not exactly `true` or `false`, is `400 invalid_params`; `approved: false` alone is a fresh turn. ### Conversation without a prompt An OpenAI-style body is a valid agent call — the model answers from the whole conversation: ```bash curl -s "https://api.plungeai.com/v1/agents/one-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"messages":[{"role":"user","content":"My name is Ada."},{"role":"assistant","content":"Hello Ada."},{"role":"user","content":"Reply with only my name."}],"max_tokens":16}' ``` ### Sync execute (default) ```bash curl -s "https://api.plungeai.com/v1/agents/one-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"prompt":"Reply with only the word: pong","max_tokens":24}' ``` Success (`200`): `{ "content": "...", "workflow_id": "...", "task_id": "t1", "request_id": "..." }`. `request_id` equals the trace id (see [Traces](/one-api/traces-observability)). ### Model / provider pinning Pin a model and provider together; discover models with `GET /v1/models` ([Models plane](/one-api/models)) or `GET /v1/discovery/search?kind=models` ([Discovery](/one-api/discovery)). ```bash curl -s "https://api.plungeai.com/v1/agents/one-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"prompt":"Reply with only: pong","provider":"openai","model":"gpt-4o-mini","max_tokens":16}' ``` ## 9.4 Async execution (202 + poll) Send `"sync": false`. The response is `202` with a pointer `{ workflow_id, task_id, request_id }`; read the result from the result route once it is ready. ```bash RESP=$(curl -s "https://api.plungeai.com/v1/agents/one-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"Reply with only the word: pong","max_tokens":24,"sync":false}') echo "$RESP" WF=$(printf '%s' "$RESP" | sed -n 's/.*"workflow_id":"\([^"]*\)".*/\1/p') TASK=$(printf '%s' "$RESP" | sed -n 's/.*"task_id":"\([^"]*\)".*/\1/p') for i in 1 2 3 4 5 6 7 8; do R=$(curl -s "https://api.plungeai.com/v1/agents/results/$WF/$TASK" \ -H "Authorization: Bearer $PLUNGE_API_KEY") case "$R" in *'"content"'*) echo "$R"; break;; *) sleep 2;; esac done ``` ## 9.5 Results route `GET /v1/agents/results/:workflowId/:taskId` → `{ content, content_type, workflow_id, task_id }`. It reconciles the run's terminal state (`lib/execution-row.ts`): | Situation | Status | Code | |---|---|---| | result stored | `200` | — | | run still in flight | `404` | `not_ready` | | run failed | `409` | `execution_failed` | | run completed but produced no visible content | `422` | `empty_completion` (raise `max_tokens ≥ 64`) | ## 9.6 Streaming Send `"stream": true` on `POST /v1/agents/:id/execute`. The agents plane emits an **OpenAI-shaped** chunk stream (the only stream shape on this plane) — `Content-Type: text/event-stream`, `data:` lines only (no named `event:` lines), each a `chat.completion.chunk`, terminated by `data: [DONE]`. This mirrors the models plane; see [Streaming](/one-api/streaming). Relay + encoder: `lib/stream-relay.ts` (`relayEngineStream`, `openaiEncoder`). Exact request shape: ```bash curl -sN "https://api.plungeai.com/v1/agents/one-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"prompt":"Reply with only the word: pong","max_tokens":24,"stream":true}' ``` Observed wire (live-verified 2026-09-18): ```text data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":…,"model":"gemini-3.1-flash-lite","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]} data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"pong"},"finish_reason":null}]} data: {"id":"chatcmpl-…","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":72,"completion_tokens":1,"total_tokens":73}} data: [DONE] ``` Notes: `model` is the pinned model, else the provider's default (never the agent id); a final `usage` is included when the provider reported the token split. On an error **after** the first byte the stream emits `data: {"error":{…}}` and closes **without** `[DONE]` (OpenRouter behaviour); a pre-dispatch rejection is a JSON error (see 9.8), not a stream. ## 9.7 The concierge approval flow `concierge` runs a bounded Discover→Inspect→Execute loop with three tools — `find_agent` (discover), `agent_card` (inspect), `run_agent` (execute) — and a payment gate (`agents/agents/concierge-agent/README.md`). Money operations (`buy`, `purchase`, `send`, `transfer`, `pay`, `withdraw`, `shop`, …) and any card `gated:<op>` are held for approval: 1. A gated action returns `{ "needs_approval": true, "pending_action": {…}, "action_token": "<10-char>", "messages": [...] }`. 2. Re-invoke with the same `messages`, plus `"approved": true` and the `action_token`, to execute and continue. The token is held server-side for 1 hour. `ask_user` questions work the same way (append the user's answer to `messages`). On the One API this handshake is surfaced through the [MCP plane](/one-api/mcp): the pause arrives as a ⏸ `continuation`, and you approve with `plungeai_continue { approve: true }` (never approve on the user's behalf). The **raw agents plane takes the same pair** (`routes/agents.ts`): send the next call with the same `messages`, `"approved": true` and the `action_token` (1-128 characters; a half pair is `400 invalid_params`) and the agent checks the token against your key's pending actions. You can also approve by prompt: a paused reply ends with `Running outside the chat? Re-run with input: approve <token>`, so send `{"prompt":"approve <token>"}` as the next call and the held action runs for your key. An unknown, used or expired token is a graceful no-op that runs no model and spends nothing — the example uses one: ```bash curl -s "https://api.plungeai.com/v1/agents/concierge/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"prompt":"approve 0000000000","max_tokens":64}' ``` (A real approval run can spend money, so it is not run here.) ## 9.8 Errors Pre-dispatch and post-dispatch errors from `routes/agents.ts` / `routes/_util.ts`. Full envelope and header reference: [Errors, limits & headers](/one-api/errors-limits). | Status | Code | When | |---|---|---| | `400` | `missing_prompt` | no `prompt`/`input` string | | `400` | `invalid_format` | `format` is not `json`/`yaml`/`markdown`/`text` | | `404` | `unknown_agent` | no such agent id (fenced before dispatch) | | `403` | `agent_not_active` | the card is not `status:active` | | `422` | `unknown_model` | pinned `model` absent/inactive in the catalog | | `422` | `empty_completion` | run completed but produced no visible content | | `422` | `invalid_params` | agent outcome `needs_input` | | `424` | `connection_required` | outcome `needs_connection` (connect an account — chapter 7) | | `424` | `credential_required` | outcome `needs_api_key` | | `503` | `provider_unconfigured` | the provider has no API key configured on the platform | | `503` | `agent_unavailable` | outcome `unavailable` | | `502` | `engine_error` / `execution_failed` | the engine/agent failed | | `502` | `result_unavailable` | run finished but the result could not be retrieved | | `409` | `duplicate_execution_id` | a replayed `x-trace-id` UUID | | `413` | `payload_too_large` | body over the cap (default 1 MiB) | ```bash curl -s "https://api.plungeai.com/v1/agents/no-such-agent-xyz/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"hi","max_tokens":8}' ``` # Workflows plane Source: https://www.plungeai.com/docs/one-api/workflows The workflows plane runs a **CNL** workflow — one or many agent tasks orchestrated by the engine — and hands back a pointer to the result. Reach it with an `ozk_` key ([Authentication](/one-api/authentication)) at `POST /v1/workflows/execute` (inline) or `POST /v1/workflows/:id/execute` (saved). Base URL is always `https://api.plungeai.com`. Source: `orchestration/api-gateway/routes/workflows.ts`, engine `orchestration/cnl-engine/`. Where the [Agents plane](/one-api/agents) runs one task, the workflows plane runs a whole DAG: parallel fan-out, sequential stages, data flow between tasks, and bounded autonomous `type: harness` missions. ## 10.1 CNL in one page **CNL** (the YAML workflow language) has a `workflow` with a `name` and a `tasks[]` array. Each task is either a leaf task or a compound block; a top-level `input:` (sibling of `workflow:`) seeds the run. Field names are from `orchestration/cnl-engine/context.ts`, `schema-types.ts` and the real files in `workflows/*.yaml`. | Field | On | Meaning | |---|---|---| | `name` | `workflow` | workflow name | | `tasks[]` | `workflow` | ordered list of tasks/blocks | | `type` | task | `task` \| `parallel` \| `sequential` \| `harness` \| `dynamic` \| `batch` \| `debate` (the `TaskType` enum also has the legacy `agent_loop` and internal `output` / `validate`) | | `id` | task | task id — the key its output is stored under | | `agent` | leaf task | the agent id to run (must be `status:active`, or the run is fenced 404/403) | | `prompt` | leaf task | the task input; search agents take the **literal** query | | `operation` | leaf task | a named operation for a structured tool-agent (e.g. `serpapi-agent` `operation: amazon`) | | *(agent params)* | leaf task | any extra card parameters as task fields (e.g. `num: 15`, `searchType: shopping`) | | `subtasks[]` | compound block | children of a `parallel` / `sequential` / … block | **Data flow.** There is no `depends_on` — a reference *is* the dependency: - `{input}` injects the workflow-level input. - `{data:<task_id>}` injects a prior task's stored output into an LLM prompt. A `sequential` block runs its `subtasks` in order; a `parallel` block runs them at once (`Promise.all`, no width cap — see the engine README "Parallelism"). Reference an upstream task and the engine orders around it. A real 3-task workflow — a parallel search fan-out feeding one LLM synthesis (adapted from `workflows/amazon-price-comparison.yaml`): ```yaml workflow: name: price-check tasks: - type: parallel id: search_phase subtasks: - type: task id: amazon_search agent: serpapi-agent operation: amazon prompt: "{input}" num: 15 - type: task id: shopping_search agent: serper-agent searchType: shopping prompt: "{input}" - type: task id: recommendation agent: one-agent prompt: | For the search "{input}", pick the top 3 products. Amazon results: {data:amazon_search} Shopping results: {data:shopping_search} Use only links that appear verbatim in the data. input: "wireless headphones" ``` ## 10.2 Inline execute `POST /v1/workflows/execute` runs one body in three sizes (`routes/workflows.ts`): a bare agent call, a `workflow:` document, or the JSON twin of either. `Content-Type` decides YAML or JSON; both produce the same object. **A bare agent call** — `agent:` plus the agent's own fields, exactly as its README documents them. The door lifts it to a one-task workflow (`{ workflow: { name: <agent>, tasks: [{ id: "call", type: "task", agent, …fields }] } }`; a pre-built agent becomes a `harness` task with the `prompt` as its goal). Top-level `format:`, `input:` and `inputs:` stay the wire's, as in every other body; `stream:`, `sync:`, `dry_run:` and `metadata:` are wire keys too and never reach the agent. ```bash curl -s -X POST "https://api.plungeai.com/v1/workflows/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF' agent: calculator-tool-agent expression: "7*6" format: markdown EOF ``` The JSON twin is `{"agent":"calculator-tool-agent","expression":"7*6"}` → the JSON envelope below with the result in `content`. Two refusals guard the lift: `agent:` next to `workflow:`/`tasks:` → `400 ambiguous_body` (send one); a field named like an engine task key (`id`, `type`, `agent`, `depends_on`, `mission_ref`) → `400 reserved_field` naming the fields. An unknown agent id → `404 unknown_agent` here (on `POST /v1/execute`, [16.9](/one-api/bots#169-the-one-door), the same body is `404 agent_not_found`). Every execute door refuses a body it cannot read, on the negotiated wire: malformed JSON → `400 invalid_json` (with the parser's position), malformed YAML → `400 invalid_yaml`, a scalar or list root → `400 invalid_body`; an empty body is *missing* (`missing_workflow`, `missing_prompt`, `invalid_params`), not malformed. **A `params:` wrapper.** On the one door, `POST /v1/execute` ([16.9](/one-api/bots#169-the-one-door)), the fields may also come wrapped in `params: {…}`, the shape the tools plane uses: the door unwraps them into the agent's fields, and a key inside `params` wins over a bare field of the same name (`routes/execute.ts`). The wrapper carries agent fields only. `params` naming `agent`, `workflow`, `tasks`, `on_behalf_of` or a wire key (`format`, `input`, `inputs`, `user_context`, `stream`, `sync`, `dry_run`, `metadata`) is `400 invalid_body`, because those keys stay at the root. A `params` that is not an object stays an ordinary field. Live-verified 2026-10-14: ```bash 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: calculator-tool-agent params: expression: "7*6" format: markdown EOF # → # Calculator: 7*6 … ## 2. Result 42 ``` **A workflow** — the full `workflow:` document as a `text/yaml` body, or JSON `{ "workflow": <object | YAML string>, "input"?: any, "inputs"?: any }`; a JSON body with top-level `name`/`tasks` auto-nests under `workflow`. A body with none of `workflow`, `agent` or `tasks` → `400 missing_workflow`. Every execute body — inline, saved (10.4) and both `execute-stream` variants — also takes: | Field | Type | Meaning | |---|---|---| | `user_context` | object · optional · default: derived from your request's network location (time zone, approximate place) | the caller's time zone, locale and location (`{time_zone, locale, place, latitude, longitude, accuracy_m, source}`, the `UserContext` schema); the agent uses it for local time and as the default schedule time zone. A wire key — it never reaches a bare agent call's fields. A raw YAML workflow body always takes the network default | Success carries the pointer and, once the sync run has stored it, the final task's `content`: `{ success: true, status, workflow_id, final_task_id, request_id, content? }`. `status` is `"completed"`, or `"incomplete"` when the final task stopped at its turn budget before `task_complete` — the stored result is then partial (the results route returns it with a `[run stopped at the N-turn budget …]` note), and the execution row carries `outcome: incomplete`. The result route (10.5) reads the same content back later, on any wire. ```bash curl -s "https://api.plungeai.com/v1/workflows/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"workflow":{"name":"doc-demo","tasks":[{"type":"task","id":"t1","agent":"one-agent","prompt":"Reply with only the word: pong"}]},"input":"go"}' ``` The same run as a raw YAML body (the response mirrors the request Content-Type → YAML, see 10.6): ```bash printf 'workflow:\n name: doc-demo-yaml\n tasks:\n - type: task\n id: t1\n agent: one-agent\n prompt: "Reply with only the word: pong"\n' \ | curl -s "https://api.plungeai.com/v1/workflows/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- ``` ## 10.3 Streaming — `execute-stream` `POST /v1/workflows/execute-stream` (same body shapes) forces the engine to stream and relays every engine SSE frame **verbatim**, plus a relay-injected `event: token` per provider delta (`lib/stream-relay.ts` `workflowsEncoder`). This is the named-event stream (distinct from the OpenAI-shaped agent stream in [Streaming](/one-api/streaming)). Event vocabulary (`context.ts` `CNLEventType`): | Event | When | Payload (`data:`) | |---|---|---| | `request_received` | run accepted | request metadata | | `workflow_loaded` | workflow parsed | workflow name/id | | `workflow_started` | execution begins | workflow id | | `task_dispatched` | a task is sent to its agent | `{task_id, agent, streaming?}` | | `streaming_started` | a task's provider stream opened | `{task_id, stream_url}` | | `token` | a provider delta (relay-injected) | `{task_id, delta}` | | `streaming_completed` | a task's provider stream closed | `{task_id}` | | `task_completed` | a task finished | `{task_id, …}` | | `task_error` | a task failed | `{task_id, error}` | | `output_delivered` | a task output was stored | `{task_id}` | | `parallel_start` / `parallel_complete` | a `parallel` block boundary | block id | | `sequential_start` / `sequential_complete` | a `sequential` block boundary | block id | | `batch_*` / `debate_*` / `dynamic_*` | block-specific progress | block payload | | `condition_evaluated` | a task `condition` was resolved | `{result}` | | `heartbeat` | idle keepalive | — | | `workflow_completed` | **outcome (success)** | `{final_task_id, execution_summary}` | | `workflow_error` | **outcome (failure)** | `{error}` | | `workflow_result` | **last frame**, after the outcome | `{final_task_id, …}` | The outcome frame is `workflow_completed` (or `workflow_error`); `workflow_result` is always the last frame, on success and on failure — a client that closes on the outcome frame misses it. Live-verified 2026-10-04 the stream carried: `request_received`, `workflow_loaded`, `workflow_started`, `task_dispatched`, `task_completed`, `workflow_completed`, `workflow_result`. ```bash printf 'workflow:\n name: stream-demo\n tasks:\n - type: task\n id: t1\n agent: one-agent\n prompt: "Reply with only the word: pong"\n' \ | curl -sN "https://api.plungeai.com/v1/workflows/execute-stream" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- ``` ## 10.4 Saved workflows A saved workflow has an id in the engine's KV (`workflow:{user}:{id}`). Create one in **Studio** or over the [MCP plane](/one-api/mcp) (`plungeai_build_workflow` from a goal, or `plungeai_workflow` `action: create`). Then run it by id: ```bash # not run here — requires a saved workflow id you own curl -s "https://api.plungeai.com/v1/workflows/<workflow_id>/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"input":"quarterly numbers"}' ``` Body: `{ "input"?: any, "inputs"?: any }`. An unknown id → `404 workflow_not_found`. A streaming variant `POST /v1/workflows/:id/execute-stream` exists with the same body and the 10.3 event vocabulary. ## 10.5 Results route `GET /v1/workflows/results/:workflowId/:taskId` → `{ content, content_type, workflow_id, task_id }`, with the same reconciliation as the agents plane: `404 not_ready` (in flight), `409 execution_failed`, `422 empty_completion`. ```bash RESP=$(curl -s "https://api.plungeai.com/v1/workflows/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"workflow":{"name":"res-demo","tasks":[{"type":"task","id":"t1","agent":"one-agent","prompt":"Reply with only the word: pong"}]}}') WF=$(printf '%s' "$RESP" | sed -n 's/.*"workflow_id":"\([^"]*\)".*/\1/p') FT=$(printf '%s' "$RESP" | sed -n 's/.*"final_task_id":"\([^"]*\)".*/\1/p') for i in 1 2 3 4 5 6 7 8; do R=$(curl -s "https://api.plungeai.com/v1/workflows/results/$WF/$FT" \ -H "Authorization: Bearer $PLUNGE_API_KEY") case "$R" in *'"content"'*) echo "$R"; break;; *) sleep 2;; esac done ``` ## 10.6 Response formats One negotiated payload, four wires (`lib/format.ts`): `json` (default), `yaml`, `markdown`, `text`. Precedence: `body.format` / `?format=` → `Accept` header → the request Content-Type mirror (`text/yaml` → `yaml`, else `json`). An explicit unknown `format` value → `400 invalid_format`; an unknown `Accept` type falls through to the mirror. `markdown` renders through the same renderer the [MCP plane](/one-api/mcp) uses; `text` is the bare result content. On the YAML wire the envelope fields travel **inside the document**: top-level `format:`, `input:` and `inputs:` next to `workflow:` in a `text/yaml` body (inline execute and execute-stream; the saved-workflow execute takes the same three in a YAML or JSON body) mean exactly what the JSON frame's fields mean — `format` has body precedence, `input` is substituted for `{input}`, `inputs` is the named map — and are stripped before the engine sees the workflow (`routes/_util.ts` `splitRequestEnvelope`). Every wire carries the final task's content once the **sync** execute routes have stored it: `markdown` and `text` answer with it as the body (the pointer keys still render when it is not stored yet); `json` and `yaml` add it as `content` next to the unchanged pointer fields. ## 10.7 The `type: harness` task A `type: harness` task with `agent: one-agent` binds an inline **mission** onto one-agent's loop mode — a bounded ReAct agent with a tool fence, iteration cap, long-term memory, and capability injection. All fields and a full example are in [Skills, plugins & capability injection](/one-api/skills-plugins-capabilities); in short: ```yaml workflow: name: research-brief tasks: - type: harness id: brief agent: one-agent goal: "One-paragraph brief on the latest Anthropic model; every claim a source URL." mission: "You are a research analyst. Produce sourced, factual briefs." allowed_tools: [web_search, web_fetch, task_complete] success_criteria: - "Every factual claim is followed by a source URL." max_iterations: 8 skills: [research] ``` ## 10.8 Legacy aliases, limits & headers **Deprecated aliases** (same auth, same workflow-plane handlers; prefer `/v1/workflows/*`): `POST /v1/cnl/execute`, `POST /v1/cnl/execute-stream` (`api-gateway.ts`). **`POST /v1/execute` is not an alias.** It is the Agent Call door, level 3 of the API ([The one door](/one-api/bots#169-the-one-door)). It still takes every inline body of 10.2 (a `workflow:`, root `tasks:`, a bare building-block agent) and adds `agent: <saved id>[@n]`, `dry_run` and `stream`. An unknown `agent:` there is `404 agent_not_found`; a task inside a `workflow:` body that names an unknown agent is still `404 unknown_agent` (below). The level 2 routes of this chapter are unchanged. **Limits** (see [Errors, limits & headers](/one-api/errors-limits) for the full table): - Body cap: `MAX_REQUEST_SIZE`, default **1 MiB** → `413 payload_too_large`. - Replayed `x-trace-id` UUID → `409 duplicate_execution_id` (send a fresh UUID or omit the header). - `parallel` fan-out has no engine-side width cap (`Promise.all`); throughput is bounded by the downstream agent service, not the engine. - `400 invalid_workflow` for a malformed CNL document; `404 unknown_agent` / `403 agent_not_active` when a task names an unknown or non-active agent (fenced before the run opens). - Every response carries `x-request-id` (= the trace id); async/YAML/markdown responses also carry `X-Execution-Id`. See [Traces & observability](/one-api/traces-observability). ## 10.9 Cancelling a run `POST /v1/workflows/executions/:id/cancel` stops a running execution. Cancellation is **cooperative** (`core/agent-core/cancel.ts`, `routes/workflows.ts`): it writes a marker in SharedMemory that the engine reads between tasks and the harness loop reads at the start of each turn and before each tool batch, then stamps the row `cancelled`. An in-flight model or tool call is not interrupted — the run stops at the next boundary, so cancellation may lag one turn. Because `POST /v1/workflows/execute` is synchronous (it returns when the run is done), cancel is for runs you can reach from another request. Pass your own `x-trace-id` (a UUID) on the execute call — it becomes the execution id — and use that id here from a second client. | Response | Meaning | |---|---| | `200 { success: true, cancelled: <id>, markers: [<id>, …] }` | Marker written under each run id (row id + the engine's cln id when it differs); row stamped `cancelled`. | | `404 not_found` | No such execution for this key. A run owned by another key answers 404 too — the API never confirms a foreign run exists. | | `409 not_running` | The execution already finished, failed, or was cancelled. | This example mutates state (it cancels a run), so it is not auto-verified: ```bash # Start a run with a trace id you choose, then cancel it from elsewhere with that same id. TRACE=$(uuidgen) curl -s "https://api.plungeai.com/v1/workflows/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -H "x-trace-id: $TRACE" \ -d '{"workflow":{"name":"cancel-demo","tasks":[{"type":"task","id":"t1","agent":"one-agent","prompt":"Count slowly to a thousand."}]}}' & curl -s -X POST "https://api.plungeai.com/v1/workflows/executions/$TRACE/cancel" \ -H "Authorization: Bearer $PLUNGE_API_KEY" # → {"success":true,"cancelled":"<TRACE>","markers":["<TRACE>"]} ``` Over MCP the same action is `plungeai_executions` with `action: "cancel"` and the `execution_id` (`orchestration/mcp-gateway/tools.ts`): a foreign or unknown id answers `Execution not found.`, a finished run answers that it is not running, and a running one is stopped at its next boundary. # MCP plane Source: https://www.plungeai.com/docs/one-api/mcp The MCP plane is bidirectional: **inbound**, the platform *is* an MCP server you point a client at; **outbound**, you connect the platform to *other* MCP servers and call their tools during a run. Both use an `ozk_` key ([Authentication](/one-api/authentication)). Base URL is `https://api.plungeai.com`. Source: `orchestration/api-gateway/routes/mcp.ts`, `orchestration/mcp-gateway/`, `orchestration/mcp-executor/`. ## 11.1 Inbound — the platform as an MCP server `POST /v1/mcp` is a Streamable-HTTP MCP endpoint that forwards your `ozk_` key to the mcp-gateway, which validates it again (the key's `allowed_tools`, `rate_limit` and `allowed_ips` apply). It is **stateless** — no `Mcp-Session-Id` is required (send one and it is forwarded). Authenticate with an `ozk_` bearer. It speaks plain JSON-RPC: `initialize`, then `tools/list` / `tools/call`. Protocol version `2025-11-25`; the older revisions `2025-06-18`, `2025-03-26`, `2024-11-05` and `2024-10-07` are negotiated ([MCP developer guide](https://mcp.plungeai.com/docs#43-protocol-version-and-initialize)). `https://mcp.plungeai.com/v1` is the **same** MCP server (the gateway's own hostname); `https://api.plungeai.com/v1/mcp` is the One API route to it with an `ozk_` bearer. Use whichever your client makes easiest. A convenience listing: a plain `GET`, no JSON-RPC to write. The answer is still the JSON-RPC `tools/list` envelope, so the tools are under `result.tools`: ```bash curl -s "https://api.plungeai.com/v1/mcp/tools" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Accept: application/json' ``` The same over JSON-RPC (what an MCP client sends): ```bash curl -s "https://api.plungeai.com/v1/mcp" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` ### The 20 `plungeai_*` tools `tools/list` returns 20 tools (live-verified 2026-09-18), in this order (`orchestration/mcp-gateway/server.ts`): | Tool | One line | |---|---| | `plungeai_execute_workflow` | run a CNL workflow (saved id or ad-hoc YAML); `mode:"async"` for long runs | | `plungeai_execute_agent` | run one prompt-driven agent (e.g. `one-agent`), optionally with a `session_id` | | `plungeai_get_tool_contract` | the exact invocation contract for one agent (params schema, operations, live credential status) | | `plungeai_execute_tool` | run one structured tool-agent with typed `{agent_id, operation?, params}` | | `plungeai_run_mission` | run a bounded autonomous mission (goal + optional fence/iteration cap); async by default | | `plungeai_learn` | manage your private skill library — `learn` / `list` / `forget` | | `plungeai_list_agents` | discover platform building-block agents (the capability registry) | | `plungeai_list_workflows` | list **your saved workflows** (what users call "my agents") | | `plungeai_get_result` | fetch one execution's result (full conversation thread for chat runs) | | `plungeai_get_workflow_status` | check an execution's status; returns a `continuation` when paused | | `plungeai_workflow` | manage workflows — create/get/update/delete/versions | | `plungeai_executions` | browse execution history — list/get/output/conversation/delete | | `plungeai_followup` | ask a follow-up about a completed execution | | `plungeai_continue` | continue a paused run — answer (`message`) or approve (`approve:true`) | | `plungeai_chat` | persistent chat with the platform assistant — send/new/list_sessions/history | | `plungeai_build_workflow` | generate or refine a saved workflow from a goal | | `plungeai_memory` | long-term memory scoped to you — recall/remember/search_runs/get_run | | `plungeai_templates` | workflow templates — list/get/use | | `plungeai_schedule` | scheduled (cron) jobs — stats/list/get/create/update/pause/resume/delete/run_now/runs | | `plungeai_whoami` | the authenticated identity — user id, tier, key label, rate-limit window | Every parameter of every tool, with a `tools/call` example: [Tool reference](https://mcp.plungeai.com/docs#14-tool-reference). Outcomes, JSON-RPC errors, HTTP statuses and limits: [Errors, feedback & limits](https://mcp.plungeai.com/docs#13-errors-feedback--limits). ### Connecting a client - **Claude Code** (one command): `claude mcp add --transport http plungeai https://mcp.plungeai.com/v1 --header "Authorization: Bearer <ozk key>"` - **Cursor / VS Code / JSON-configured clients** — an `.mcp.json` server entry with `"url": "https://mcp.plungeai.com/v1"` and an `Authorization: Bearer` header. - **Claude Desktop** — the connector UI cannot set a bearer header, so bridge with `mcp-remote https://mcp.plungeai.com/v1 --header "Authorization: Bearer <ozk key>"`. **claude.ai (web)** gets a PlungeAI skill upload only, with no live tools ([Connecting clients](https://mcp.plungeai.com/docs#33-claudeai-and-claude-desktop)). - **Any client** — point it at either URL; it is plain JSON-RPC over Streamable HTTP. The install page `https://www.plungeai.com/install` generates ready-made config. - **Every supported client**, with its config and how to verify it: [Connecting clients](https://mcp.plungeai.com/docs#3-connecting-clients). ## 11.2 Outbound — MCP runs against other servers The outbound surface connects the platform to registry MCP servers, lists their tools, calls them, and tears the run down. Secrets are resolved **server-side** in the mcp-executor (never in your request): `${VAR}` → a platform secret; `${{ credentials.<provider> }}` → the acting user's BYOK key / OAuth token, a miss becoming a structured `needs_connection` (`orchestration/mcp-executor/CLAUDE.md`). Each run is a per-tenant Durable Object keyed `customerUuid:runId`; background runs are held in resumable ~10-minute legs (the Ten-Minute Relay). Discover connectable servers with `GET /v1/discovery/search?kind=mcp-servers` ([Discovery](/one-api/discovery)). | Route | Body | Returns | |---|---|---| | `POST /v1/mcp/runs` | `{ "server_ids": ["<id>", …] }` (non-empty string array) | `201 { run_id, tools, connected, failed, warnings, skipped }` | | `GET /v1/mcp/runs/:id/tools` | — | `{ tools, count }` | | `POST /v1/mcp/runs/:id/call` | `{ "name": "mcp__<server>__<tool>", "args"?: {…}, "timeout_ms"?: n }` | `{ content }` | | `DELETE /v1/mcp/runs/:id` | — | `{ success: true }` | Notes: - `server_ids` are registry `mcp-server` catalog ids — **not** arbitrary URLs; the executor resolves each server's manifest and credentials. No `url`/`auth` is accepted in the body. - Tool names are namespaced `mcp__<serverId>__<tool>` so the loop's fence and per-tool permissions apply to MCP tools like native ones. - A server that just failed is skipped for the next run for 60s (per-tenant cooldown → `skipped`). If none connect → `502 no_servers_connected` with `failed`/`warnings`/`skipped`. A tool call that fails → `502 tool_call_failed`; a missing `name` → `400 invalid_body`. - Always `DELETE` the run when done (the DO also self-cleans a stale run). A full cycle against the credential-free `cloudflare-docs` server — create, list tools, delete: ```bash RID=$(curl -s "https://api.plungeai.com/v1/mcp/runs" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"server_ids":["cloudflare-docs"]}' \ | sed -n 's/.*"run_id":"\([^"]*\)".*/\1/p') echo "run_id=$RID" curl -s "https://api.plungeai.com/v1/mcp/runs/$RID/tools" \ -H "Authorization: Bearer $PLUNGE_API_KEY" echo curl -s -X DELETE "https://api.plungeai.com/v1/mcp/runs/$RID" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` To **call** a tool on the run (not run here — it does real work): ```bash # not run here curl -s "https://api.plungeai.com/v1/mcp/runs/<run_id>/call" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"name":"mcp__cloudflare-docs__search_cloudflare_documentation","args":{"query":"durable objects"}}' ``` ## 11.3 MCP servers inside a harness mission Outbound runs are the *manual* surface. Inside a bounded mission, list the servers on the task's `mcp:` field and the loop host connects them for the run (through the same mcp-executor), exposing their tools to the agent under the fence. See [Skills, plugins & capability injection](/one-api/skills-plugins-capabilities) for the `mcp:` field and a full `type: harness` example, and the [Workflows plane](/one-api/workflows) for running it. In-loop MCP is the inbound path for our agents; the `POST /v1/mcp` endpoint above is the outbound control plane for external clients — the two never cross. # Traces & observability Source: https://www.plungeai.com/docs/one-api/traces-observability Every One API response is traceable. This chapter covers the trace id, the `GET /v1/traces/:id` read, execution history and cost, the response headers, and what is **not** observable through the API today. Base URL is `https://api.plungeai.com`; reads use an `ozk_` key ([Authentication](/one-api/authentication)). Source: `orchestration/api-gateway/routes/traces.ts`, `orchestration/api-gateway/lib/execution-row.ts`. ## 12.1 The trace id Every response carries **`x-request-id`**, the router's id for that one request (on the models plane the inference gateway's, [13.8](/one-api/errors-limits#138-response-headers)). A run's **trace id** is its run id: `workflow_id` / `execution_id` in the body, `x-execution-id` where the response sets it, and `workflow_id` on every SSE frame. Without `x-trace-id` the router uses its request id as the trace id, so `x-request-id`, `x-execution-id`, `workflow_id` and the body's `request_id` are one UUID (live-verified 2026-09-18 for one agent execute) and you can trace a run from any of them. You may also **supply** the trace id: send `x-trace-id: <uuid>` and the run adopts it (a replayed UUID that already ran → `409 duplicate_execution_id`, see [Errors](/one-api/errors-limits)). `x-request-id` and the body's `request_id` then stay the router's per-request id: read the trace by the id you sent (it is also the run's `workflow_id`), not by `request_id`, which answers `404` (live-verified 2026-10-04: `GET /v1/traces/<x-trace-id>` → `200`, `GET /v1/traces/<x-request-id>` → `404`). ## 12.2 Response headers | Header | On | Meaning | |---|---|---| | `x-request-id` | every response | the router's request id; the trace id unless you sent `x-trace-id` ([12.1](#121-the-trace-id)) | | `x-execution-id` | execution responses | the run id, which is the trace id | | `x-correlation-id` | when you send `x-request-id` | your `x-request-id`, echoed (it never becomes an id) | | `x-error-code` | error responses | the machine error code (mirrors `error.code`) | | `server-timing` | every response | `total;dur=<ms>;desc="Total Response Time"` | | `x-content-type-options` | every response | `nosniff` | The execution planes emit no `x-ocean-*` headers; the models plane adds `x-ocean-router-id`, `x-ocean-cost-usd` and `x-ocean-dropped-params`. Every header and the eleven a browser may read (CORS expose list) are in [13.8](/one-api/errors-limits#138-response-headers). Rate-limit state is not returned as a header — read it with `plungeai_whoami` (the [MCP plane](/one-api/mcp)) or see [Authentication](/one-api/authentication). ## 12.3 `GET /v1/traces/:id` Reads one trace by id and joins two telemetry tables (both on `ocean-telemetry` via `TELEMETRY_DB`, per the root `CLAUDE.md` DB map): ```json { "trace_id": "<id>", "spans": [ … ], "gateway_requests": [ … ] } ``` - **`spans`** — rows of `execution_trace_spans` (columns: `id, trace_id, workflow_id, task_id, ts, type, agent, status, duration_ms, payload_ref, payload_inline`), ordered by time, up to 500. These are written by `trace-service` off the async trace-events queue, so they can **lag** a just-finished run or be empty on a short one. - **`gateway_requests`** — rows of `api_gateway_requests` (columns: `id, trace_id, plane, route, user_id, status, upstream, duration_ms, cost_usd, created_at`): the router's own request log — one row per API call on this trace, including the plane, route, HTTP status and per-request cost. An **unknown** trace id — and a trace that belongs to another tenant — is `404 not_found`, the same answer for both, so a foreign id is never confirmed. You own a trace when your key started the run (its execution row is written synchronously, before the engine dispatch) or made the first request logged under it (tool calls). A run that just finished is therefore `200` even while its `spans` are still empty — those are queue-written and can lag. A request's own log row lands a moment after its response: a `404` for an id you have only just used means "nothing recorded yet", so retry in a few seconds. Execute without `x-trace-id`, then read the trace by its `request_id` (here it is the trace id): ```bash RESP=$(curl -s "https://api.plungeai.com/v1/agents/one-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"prompt":"Reply with only: pong","max_tokens":24}') RID=$(printf '%s' "$RESP" | sed -n 's/.*"request_id":"\([^"]*\)".*/\1/p') echo "trace=$RID" sleep 3 TRACE=$(curl -s "https://api.plungeai.com/v1/traces/$RID" \ -H "Authorization: Bearer $PLUNGE_API_KEY") echo "$TRACE" echo "$TRACE" | grep -q "\"trace_id\":\"$RID\"" && echo TRACE_MATCH_OK ``` An unknown trace id — `404 not_found`: ```bash curl -s "https://api.plungeai.com/v1/traces/00000000-0000-4000-8000-000000000006" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ## 12.4 Execution history & cost Every agent and workflow run opens a `workflow_executions` row on `ocean-db` (`DB`), `source: 'api'` (`lib/execution-row.ts`). The row carries `status` (`running` → `completed` / `failed`), `input`, `workflow_name`, `duration`, `end_time`, `execution_summary` (the per-task durations the pricer reads) and, once priced, the run cost. It is closed from the engine's response after the run; a run the platform cannot record does not run "for free". Where a developer sees these rows: - **MCP plane** — `plungeai_executions` (`list` / `get` / `output` / `conversation`) and `plungeai_get_result` return your run history and results as user-ready markdown ([MCP plane](/one-api/mcp)). - **Studio** — your runs appear in the run history and per-run views. - **Dashboard** — `dashboard.plungeai.com` surfaces run status and cost roll-ups. Cost is computed asynchronously by the workflow-cost worker from `execution_summary`; a run whose summary was preserved is priced (an empty-completion or retrieval fault is flipped to `failed` **without** wiping the summary, so the reasoning tokens it spent are still metered). ## 12.5 What is not observable via the API today - There is no public per-token or per-span streaming metrics endpoint; live progress comes from the SSE streams ([Streaming](/one-api/streaming), and the workflows event vocabulary in [Workflows plane](/one-api/workflows)). - `GET /v1/traces/:id` `spans` depend on async queue processing — treat an empty `spans` array as "not landed yet", and use `gateway_requests` (written inline) plus the execution row for the authoritative status. - There is no dedicated cost endpoint on this plane — read cost from the execution row via MCP / Studio / Dashboard (12.4). - You read only your own traces; you cannot read another tenant's trace. # Errors, limits & headers Source: https://www.plungeai.com/docs/one-api/errors-limits The platform has **two error envelopes**. The **execution planes** (tools, agents, workflows, MCP, discovery, traces) use the router envelope; the **models plane** (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`) uses the OpenAI-shaped envelope from the inference gateway. Branch on the HTTP status and, on the execution planes, the `X-Error-Code` header — not on any field's type. ## 13.1 The two envelopes **Execution planes** (`orchestration/api-gateway/lib/respond.ts`, `lib/format.ts`): ```json { "error": { "code": "unknown_agent", "message": "No such agent: foo", "…": "extra fields" } } ``` - Header `X-Error-Code: <code>` is **always** set — clients can branch on it without parsing the body. - Header `x-request-id` echoes the server-minted id. - The body is **format-negotiated**: `?format=` / `body.format` / `Accept` / a `text/yaml` content-type mirror select `json` (default), `yaml`, `markdown`, or `text`. An unknown explicit format → `400 invalid_format` (as JSON). `code`+`message` survive every format; `missing`, `contract`, `hint`/`detail`, `outcome` and the execution id render on `markdown` (the MCP renderer) and `missing` on `text`; every field survives `json`/`yaml`. **Models plane** (`inference/gateway/src/errors.ts`) — OpenAI shape, no `X-Error-Code`: ```json { "error": { "message": "Invalid API key", "type": "authentication_error", "code": "invalid_api_key", "request_id": "…" } } ``` - `code` is a **string**, `type` groups it, `request_id` is inside the body. Response headers carry `x-request-id` (and `x-correlation-id` when you sent an `x-request-id`), never `X-Error-Code`. - A known path called with the wrong method (`GET /v1/chat/completions`, `POST /v1/models`) answers `405 method_not_allowed` with an `Allow` header naming the one method it serves: | HTTP | `code` | When | Do | |---|---|---|---| | 405 | `method_not_allowed` | A models-plane path called with the wrong method | Use the method in `Allow` | ## 13.2 Execution-plane error codes | HTTP | `code` | When | Do | |---|---|---|---| | 400 | `invalid_json` / `invalid_body` / `invalid_request` | Body is not valid JSON (the message quotes the parser's position) / its root is not an object or mapping | Fix the body | | 400 | `invalid_format` | `?format=`/`body.format` is not `json\|yaml\|markdown\|text` | Use a known format | | 400 | `missing_prompt` | Agent execute without a `prompt`/`input` string and without a `messages` user turn | Send a prompt or messages | | 400 | `missing_workflow` | Workflow execute with none of `workflow`, `agent` or `tasks` | Send a `workflow:` document or a bare `agent:` body ([10.2](/one-api/workflows#102-inline-execute)) | | 400 | `ambiguous_body` | `agent:` next to `workflow:`/`tasks:` in one body | Send one of them | | 400 | `reserved_field` | A bare-agent field named like an engine task key (`id`, `type`, `agent`, `depends_on`, `mission_ref`); the body names them | Rename the field | | 400 / 422 | `invalid_workflow` | Engine rejected the workflow (structural) / a save route refused the YAML: schema or kind lint, with `errors` and `warnings` and nothing written ([16.1](/one-api/bots#161-lifecycle)) | Fix the CNL | | 400 | `invalid_yaml` | A `text/yaml` body is not valid YAML (every execute door) | Fix the YAML syntax (the message quotes js-yaml's reason and position) | | 401 | `unauthorized` | Missing/invalid/revoked `ozk_` key | Send a valid key ([Auth](/one-api/authentication)) | | 401 | `invalid_signature` | Internal HMAC identity failed | Service-to-service only | | 403 | `refused` | Trust fence — agent/tool refused ([13.5](#135-trust-fence)) | Do not retry | | 404 | `not_found` | No route for method+path | Check the path | | 403 | `agent_not_active` | The agent's card is not `status:active` (refused before dispatch) | Pick an active agent ([Discovery](/one-api/discovery)) | | 403 | `lock_admin_required` | `PUT /v1/workflows/{id}/permissions` set `locks` and the caller is not a team admin over the workflow; nothing was written ([16.2](/one-api/bots#162-policy-and-budget)) | Ask an admin | | 404 | `unknown_agent` | No such agent id on the agents, tools and workflows planes, and for a task inside a workflow body | Discover a valid id ([Discovery](/one-api/discovery)) | | 404 | `agent_not_found` | `POST /v1/execute` ([16.9](/one-api/bots#169-the-one-door)): `agent:` names no saved agent of yours (or snapshot) and no building block | Check the id, and the `@n` snapshot | | 404 | `unknown_tool` | No such tool id | Discover a valid id | | 404 | `workflow_not_found` | No saved workflow with that id for this key (a foreign id answers the same) | Check the id/owner | | 404 | `execution_not_found`, `schedule_not_found`, `trigger_not_found`, `group_not_found`, `binding_not_found`, `file_not_found`, `version_not_found`, `template_not_found`, `skill_not_found`, `memory_not_found`, `connection_not_found`, `unknown_provider` | The Bots routes ([16](/one-api/bots)) and connections ([7](/one-api/connectors)): no such row for this key. Another account's id answers the same | Check the id | | 404 | `run_not_found` | MCP run never opened, closed, or expired (30 min idle); on `GET /v1/memory/runs/{id}`, no past run with that id | Open a new run with `POST /v1/mcp/runs`; retrying never revives it | | 404 | `not_ready` | Result not stored yet (still running) | Poll again | | 409 | `approval_required` | Trust fence — human approval pending ([13.5](#135-trust-fence)) | Approve, then retry | | 409 | `execution_failed` | Result route: the run failed | Read the message | | 409 | `duplicate_execution_id` | Replayed `x-trace-id` UUID hit the run PK | Send a fresh UUID or omit it | | 409 | `not_paused`, `execution_finished` | `POST /v1/executions/{id}/continue` on a run that already completed; `steer` or `stream` on a run that is no longer running ([16.3](/one-api/bots#163-executions-and-approvals)) | Use `followup`, or read the output | | 409 | `memory_conflict`, `skill_name_taken`, `name_taken`, `binding_limit` | A memory id that is also inside another entry; a skill name held by a platform skill or another account; a bot-group name you already use; the account's binding limit ([16.6](/one-api/bots#166-workspace-inbox-bindings-groups), [16.7](/one-api/bots#167-memory-and-skills)) | Forget by text; pick another name; unbind one | | 410 | `session_expired`, `result_expired` | No conversation session is left for `continue`; the stored result of `output` is gone ([16.3](/one-api/bots#163-executions-and-approvals)) | Start a new run | | 413 | `payload_too_large` | Body over the size cap ([13.6](#136-limits-body-caps-timeouts)) | Shrink the body | | 422 | `invalid_params` | Outcome `needs_input` — agent needs different args | Fix the params | | 422 | `unknown_model` | Pinned model absent/inactive in the catalog | Discover a model | | 422 | `not_paired`, `memory_rejected`, `invalid_name`, `invalid_members` | A chat you did not pair; a memory write the store refuses (it carries `current_entries`); a bot-group name or member list that fails its checks ([16.6](/one-api/bots#166-workspace-inbox-bindings-groups), [16.7](/one-api/bots#167-memory-and-skills)) | Fix the request | | 422 | `empty_completion` | Run completed but produced no visible content | Raise `max_tokens` (≥64) | | 424 | `connection_required` | Outcome `needs_connection` — no connected account | Connect the account ([Connectors](/one-api/connectors)) | | 424 | `credential_required` | Outcome `needs_api_key` — no API key for a connector | Add the key | | 429 | `rate_limited` | Tier RPM/RPD or failed-auth cap ([13.4](#134-rate-limits)) | Honour `Retry-After` | | 500 | `config_error` | Server misconfiguration | Report with `x-request-id` | | 500 | `internal_error` | Unhandled router error | Report with `x-request-id` | | 502 | `engine_error` | Engine failed (untagged) | Retry; report if persistent | | 502 | `upstream_error`, `storage_error`, `scheduler_error`, `registry_unavailable` | Upstream service/storage error; on the Bots routes the storage, scheduler or skill registry failed and no detail is returned | Retry with backoff | | 502 | `tool_call_failed` | Tool execution failed | Read the message | | 502 | `no_servers_connected` | MCP: no server connected | Connect a server | | 502 | `result_unavailable` | Run finished but the result could not be retrieved | Retry the result route | | 503 | `not_enabled`, `unavailable`, `connections_unavailable` | A feature is not switched on in this deployment yet: channel bindings, bot groups, outbound hooks, connections ([16](/one-api/bots)) | Retry later | | 503 | `agent_unavailable` | Outcome `unavailable` — agent temporarily down | Retry later | | 503 | `provider_unconfigured` | Chosen provider has no key on this platform | Choose another provider | | 503 | `money_plane_unavailable` | Model routes not staged on this tier | Retry later | ## 13.3 Outcome → HTTP mapping Execute routes classify an agent/tool **outcome** (`routes/_util.ts outcomeStatus`). A `success: false` engine result tagged with an outcome remaps to a caller-actionable status; an untagged failure stays `502 engine_error`. | Outcome | HTTP | `code` | |---|---|---| | `needs_input` | 422 | `invalid_params` | | `needs_connection` | 424 | `connection_required` | | `needs_api_key` | 424 | `credential_required` | | `unavailable` | 503 | `agent_unavailable` | | `error` (or untagged) | 502 | `execution_failed` / `engine_error` | | `needs_approval` | 409 | `approval_required` ([13.5](#135-trust-fence)) | ## 13.4 Rate limits **Execution planes** (`orchestration/mcp-gateway/rate-limiter.ts`, a Durable Object; enforced in `lib/auth.ts`). Counted on **POST** executions only — catalog `GET`s are free. Per tier: | Tier | per minute | per day | |---|---|---| | `free` | 30 | 1,000 | | `pro` | 100 | 10,000 | | `enterprise` | 300 | 100,000 | A key with no tier is treated as `free`. A per-key `rate_limit` override is ignored on these planes (`lib/auth.ts` does not pass it to `checkRateLimit`); it applies only on `https://mcp.plungeai.com/v1`. Failed authentications are metered per client IP (`auth-fail`: 20/min, 500/day) to bound key brute-force. On a limit hit: `429 rate_limited` with a `Retry-After` header (seconds). A key is never shut out for longer than **ten minutes**: reaching the day limit pauses the key for 10 minutes (`Retry-After` ≤ 600), after which its day count starts over (2026-10-05; the minute limit waits out the minute as before). The per-tier numbers are the defaults: the operator can change them or switch the limits off (the failed-authentication meter stays on), see [2](/one-api/authentication). **Money plane** (`inference/gateway/src/auth/rate-limit.ts`). Per **key**, atomic per-minute window (`RateLimiterDO`, KV soft-cap fallback): default **600 req/min** (`RATE_LIMIT_RPM_DEFAULT`) → `429 rate_limit_exceeded`. Separate from billing caps: `429 insufficient_quota` (monthly limit / trial allowance), `402 insufficient_balance` (empty wallet), `429 spend_cap_exceeded` (guardrail) — see [3.10](/one-api/models#310-guardrails), [3.13](/one-api/models#313-billing--metering). ## 13.5 Trust fence Two statuses gate risky actions: - **`403 refused`** — the agent/tool refused the action outright (policy). Do not retry unchanged. - **`409 approval_required`** — a human must approve before the action runs. Approve out-of-band, then re-issue the request. ## 13.6 Limits: body caps, timeouts - **Body cap (execution planes):** `MAX_REQUEST_SIZE`, default **1 MiB** (`routes/_util.ts` — `1_048_576` bytes). Enforced against `Content-Length` first, then streamed; over → `413 payload_too_large`. The models plane relies on the provider/runtime body limits. - **Timeouts:** there is no gateway-imposed request timeout beyond the Cloudflare Workers runtime and each upstream's own. Streams have no idle timeout on the money/workflows plane; the agents plane sends `: OCEAN PROCESSING` keepalives ([4.3](/one-api/streaming#43-agents-plane-openai-shaped-stream)). - **Storage errors** are scrubbed: a D1/SQLite failure surfaces as `502 upstream_error` "storage error" (the raw error is logged, never leaked); internal service-binding names are redacted from messages. ## 13.7 Retry guidance - **429** — back off and honour `Retry-After`. Distinguish `rate_limited`/`rate_limit_exceeded` (throttle) from `insufficient_quota`/`spend_cap_exceeded` (billing — retrying won't help until the window resets or billing changes). - **5xx** (`502`/`503`) — retry with exponential backoff; the models plane already retries `429`/`5xx` once per candidate and fails over ([3.3](/one-api/models#33-routing-models-sort-failover)). - **4xx other than 429** — do **not** retry unchanged; fix the request. `needs_*` outcomes (`422`/`424`) are yours to resolve (params, connection, key). - Always include the `x-request-id` when reporting an error. ## 13.8 Response headers | Header | Plane | Meaning | |---|---|---| | `x-request-id` | both | Server-minted request id: the router's on the execution planes, the inference gateway's (= `error.request_id`) on the models plane. A client-sent `x-request-id` is never adopted — it comes back as `x-correlation-id`. | | `x-ocean-router-id` | models | The router's own id for a models-plane call (the id `x-request-id` carried before the two were split). | | `X-Error-Code` | execution (errors) | The error `code`, for header-only branching. | | `X-Execution-Id` | execution | The run id on results, streams, and async pointers. | | `x-correlation-id` | both | Echo of your `x-request-id` when you send one. A successful models-plane call without one carries the router's trace id here. | | `x-generation-id` | models (`/api/v1`) | The `gen-…` completion id (= the body `id`), the id `GET /generation?id=` takes ([3.6](/one-api/models#36-response-headers)). | | `x-ocean-dropped-params` | models | Request fields the routed provider shape could not take, comma-separated ([3.2](/one-api/models#32-request-fields)). | | `x-cache` | models | `hit` / `miss` on cache-eligible chat requests ([3.11](/one-api/models#311-response-cache)). | | `x-ocean-cost-usd` | models | The settled price in USD (6 decimals) of a non-stream `POST /v1/chat/completions`; `0.000000` on a response-cache hit. Absent on streams (use `usage.cost`) and on the execution planes (priced later). | | `x-ocean-agent` · `x-ocean-tasks` · `x-ocean-version` | execution (`POST /v1/execute`) | What ran: the saved agent or building block (absent for an inline workflow), how many top-level tasks, and the snapshot (`<n>`, `live` or `inline`) ([16.9](/one-api/bots#169-the-one-door)). | | `Server-Timing` | execution | `total;dur=<ms>` timing. | | `X-Content-Type-Options` | execution | `nosniff` on every router response. | | `Retry-After` | both | Seconds to wait after a `429`. | | `Access-Control-Allow-Origin` (+ CORS) | both | `*`; both planes answer `OPTIONS` preflight and advertise the same methods (`GET, POST, PUT, PATCH, DELETE, OPTIONS`). | | `Access-Control-Expose-Headers` | both | The headers a browser client may read: `x-request-id`, `x-ocean-router-id`, `x-execution-id`, `X-Error-Code`, `Retry-After`, `Server-Timing`, `x-cache`, `x-ocean-cost-usd`, `x-generation-id`, `x-correlation-id`, `x-ocean-dropped-params`, `x-ocean-agent`, `x-ocean-tasks`, `x-ocean-version`, `Content-Disposition`. | | `Allow` | models | On a `405`: the one method the path serves, plus `OPTIONS`. | | `Content-Type` | both | `application/json` \| `text/event-stream` \| negotiated `text/yaml`\|`text/markdown`\|`text/plain`. | ## 13.9 Verified examples Unauthenticated execution-plane request → `401 unauthorized` with the `x-error-code` header: ```bash curl -is "$API_BASE/v1/agents" ``` Unknown route → `404 not_found`, header `x-error-code: not_found`, plus `x-request-id`: ```bash curl -is "$API_BASE/v1/nope" ``` Agent execute without a prompt → `400 missing_prompt`: ```bash curl -s "$API_BASE/v1/agents/one-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "Content-Type: application/json" -d '{}' ``` Unknown agent id → `404 unknown_agent` (the trust/fence pre-dispatch check): ```bash curl -s "$API_BASE/v1/agents/zzz-not-an-agent/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "Content-Type: application/json" -d '{"prompt":"hi"}' ``` # Skills, plugins & capability injection Source: https://www.plungeai.com/docs/one-api/skills-plugins-capabilities The platform does not ship one agent per behaviour. It ships **one agent** — `one-agent`, whose loop mode runs the mission (see [Agents plane](/one-api/agents)) — into which **capabilities are injected per run**. *Capability injection* is the six kinds below: skills, experts, persona, backgrounds, plugins, and MCP servers. You declare them on a `type: harness` task and the loop host resolves them in one parallel pass before the run (`core/core-base/pack-enrich.ts`). Base URL is `https://api.plungeai.com`; runs use an `ozk_` key ([Authentication](/one-api/authentication)). ## 14.1 The six kinds | Kind | What it injects | Where it comes from | Eager limit | Field | |---|---|---|---|---| | **backgrounds** | always-on ambient context, prepended **first** | registry card `prompt_format` | 3 | `backgrounds: [id, …]` | | **persona** | identity / voice, prepended | persona-memory | 1 (one voice per run) | `persona: id` (aliases `digital_twin`, `personas[0]`) | | **experts** | labelled `## EXPERT: <id>` sections | expert-memory | 3 | `experts: [id, …]` | | **skills** | an `## ADDITIONAL INSTRUCTIONS` body | skill-memory, else the registry skill card | 5 | `skills: [id, …]` | | **plugins** | a bundled SKILL.md index + script index (never MCP ids) | plugin-memory | — | `plugins: [id, …]` | | **mcp** | remote MCP tools, connected for the run and exposed under the fence | mcp-executor (secrets resolved server-side) | — | `mcp: [id, …]` | Injection order into the system prompt: **backgrounds → persona → experts**, then the skill body under `## ADDITIONAL INSTRUCTIONS`. Plugin **scripts** become available to the `run_python` tool; plugin **skills** are advertised by name + description and pulled on demand with `load_skill`. MCP ids are connected by the loop host through the mcp-executor (the same machinery as the [MCP plane](/one-api/mcp) outbound runs) and their tools appear as `mcp__<server>__<tool>` under the mission's fence. ## 14.2 Limits & degradation - **Total eager budget** across backgrounds + persona + experts + skills is `EAGER_BUDGET_BYTES` = **24 KiB** (`pack-enrich.ts`). Over budget, later items are **listed by id + description instead of injected** — the agent pulls them on demand via `load_skill` (progressive disclosure). - **Per-kind caps:** `skills` 5, `experts` 3, `backgrounds` 3 (`MAX_EAGER`). Ids beyond the cap are deferred to `load_skill` the same way. - **Fail-soft:** a missing or unresolvable capability degrades to a **warning lane**, it does not fail the run. A missing MCP credential surfaces as a structured `needs_connection:<provider>`. - **Arrays replace, they don't union** — the most specific layer wins per key (14.4). ## 14.3 The skill catalog Skill sources live in `skills/<id>/SKILL.md`; the runtime injects the published **registry skill card**. The live catalog is the source of truth — discover it with `GET /v1/discovery/search?kind=skills` ([Discovery](/one-api/discovery)). Install PlungeAI's own agent skills from the [skills catalog](https://skills.plungeai.com). The ids on disk today (`skills/`): | id | one line | |---|---| | `web-search` | search the web and cite sources | | `report` | structure findings into a clean report | | `refactor` | refactor code toward a smaller, clearer shape | | `code-review` | review a diff for correctness and simplification | | `debug` | systematic root-cause debugging | | `file-ops` | read/write/search files in the workspace | | `prd` | draft a product requirements document | | `qa` | test-and-verify discipline | | `thinking` | structured reasoning before acting | | `workflow` | author a CNL workflow | | `biotech` | biotech-domain research helper | ```bash curl -s "https://api.plungeai.com/v1/discovery/search?kind=skills&q=web-search&limit=5" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` **Plugins** are bundled SKILL.md indexes + runnable scripts (not MCP servers). Discover them with `kind=plugins`; **MCP servers** (the `mcp:` kind) with `kind=mcp-servers`: ```bash curl -s "https://api.plungeai.com/v1/discovery/search?kind=mcp-servers&limit=5" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` > The discovery kinds `personas` and `experts` return cards (live-verified 2026-10-05: 20 of each on > a first page of `GET /api/registry/search?kind=personas&q=assistant`, same for `kind=experts`; > persona cards have `type: digital_twin`). The card is the catalog entry; persona/expert text is > still resolved from the persona-memory / expert-memory services at run time. ## 14.4 Pre-built agent cards — `mission_ref` / `pack` A **pre-built agent** is a registry `agent` card whose `prompt_format` holds one portable markdown document: YAML frontmatter (a typed capability pack) plus a body that *is* the system prompt, compiled on read by `core/core-base/agent-markdown.ts`. Reference it instead of writing the mission inline: ```yaml - type: harness agent: one-agent mission_ref: research-analyst # canonical; `pack:` is an alias; a single-token `mission:` also works ``` Frontmatter keys the compiler reads: `name`, `description`, `model`, `effort`, `tools`, `skills`, `experts`, `persona`, `backgrounds`, `plugins`, `mcp`, `agents` (named sub-agents), `permissions`, `success_criteria` — plus the body. Unknown keys are ignored with a warning (forward-compatible); ownership/visibility are stamped server-side, never read from frontmatter. **Merge order (per key, last wins): pre-built card → workflow root → task**; arrays replace. So a card can define the base mission and the task can override `model` or clear `skills: []`. ## 14.5 How to run it **Workflows plane (recommended)** — a `type: harness` task carries the mission and its capability fields; run it inline or save it (see [Workflows plane](/one-api/workflows)). A full example: ```yaml workflow: name: sourced-brief tasks: - type: harness id: brief agent: one-agent goal: "Write a one-paragraph brief on the latest Anthropic model; every claim a source URL." mission: "You are a research analyst. Produce sourced, factual briefs." allowed_tools: [web_search, web_fetch, task_complete] success_criteria: - "Every factual claim is followed by a source URL." max_iterations: 8 skills: [web-search, report] experts: [python-pro] persona: analyst backgrounds: [acme-corp] plugins: [finance-bundle] mcp: [brave-search] ``` `allowed_tools` is the fence (fail-closed — a tool not listed never executes); `max_iterations` (alias `max_turns`) is the loop cap; `effort` (`quick`/`standard`/`deep`) sets a default cap when no explicit number is given. **Agents plane** — `POST /v1/agents/:id/execute` forwards **only `persona`** among the capability fields (`routes/agents.ts`); `skills`, `experts`, `backgrounds`, `plugins`, `mcp`, `goal`, `allowed_tools` are **ignored** there. To inject skills/plugins/MCP, use the `type: harness` task on the workflows plane, or the [MCP plane](/one-api/mcp) `plungeai_run_mission`. A minimal harness run with a real skill injected, capped at 2 iterations: ```bash curl -s "https://api.plungeai.com/v1/workflows/execute" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"workflow":{"name":"cap-demo","tasks":[{"type":"harness","id":"h1","agent":"one-agent","goal":"Reply with the single word pong, then call task_complete.","mission":"You are a terse assistant. Answer in one word.","allowed_tools":["task_complete"],"max_iterations":2,"skills":["report"]}]}}' ``` ## 14.6 Seeing what was injected The enrichment pass reports what it resolved and what degraded (`pack-enrich.ts`): capabilities that could not be resolved appear as **warning lanes**, and those dropped for the eager budget/cap are reported as **deferred** (loadable with `load_skill`). These surface in the run's result metadata and in the trace spans ([Traces & observability](/one-api/traces-observability)); a missing skill shows up as a warning rather than silently absent behaviour. For the full runtime model — tool fence, memory, delegation, recursion guard — see `orchestration/HARNESS-README.md`. # SDKs & clients Source: https://www.plungeai.com/docs/one-api/sdks-and-clients You do not need an Ocean-specific SDK to use the One API. The models plane is OpenAI-wire, so every OpenAI-compatible client works with a base-URL swap; the execution planes are plain HTTP+JSON and have official SDKs in TypeScript, Python, Go and Rust. This chapter lists the options, shortest path first. Key throughout: `$PLUNGE_API_KEY` = `ozk_`, the one key for every plane. See [Authentication & keys](/one-api/authentication). ## OpenAI SDK (models plane) Point the official OpenAI SDK at `https://api.plungeai.com/v1` with your `ozk_` key. This covers `/v1/chat/completions`, `/v1/embeddings`, and `/v1/models`. Python: ```python from openai import OpenAI client = OpenAI( api_key="<your ozk_ key>", base_url="https://api.plungeai.com/v1", default_headers={"x-trace-id": "00000000-0000-4000-8000-000000000006"}, # optional UUID ) resp = client.chat.completions.create( model="anthropic/claude-sonnet-4-6", messages=[{"role": "user", "content": "Reply with exactly the word: pong"}], max_tokens=16, ) ``` TypeScript: ```ts import OpenAI from 'openai' const client = new OpenAI({ apiKey: process.env.PLUNGE_API_KEY, baseURL: 'https://api.plungeai.com/v1', defaultHeaders: { 'x-trace-id': crypto.randomUUID() }, // optional }) ``` Ocean-only fields (`models[]`, `sort`, `@preset/<slug>`) pass through as extra body params — use the SDK's `extra_body` (Python) or just add them to the request object (TypeScript). See [Models plane](/one-api/models). ## Official SDKs (execution planes) Four packages, one shape: a client that reads the key from `PLUNGEAI_API_KEY`, one method per operation (`client.agents.execute(…)`), typed errors by status, retries (2, with backoff, on connection errors, 408, 429 and ≥ 500 — never on 409 or 424, which are semantic answers), a 10-minute timeout, server-sent-event streams, offset pages that iterate across pages, raw-response access and `get/post/…` escape hatches. The curl examples in this guide use `$PLUNGE_API_KEY`; the SDKs' variable is `PLUNGEAI_API_KEY`, the CLI's too. All four target One API 2.5.0 (package version 1.0.0); their resource layers are generated from this document, so every operation listed here has a method. Source: `sdk/` in the platform repository. | Language | Package | Install | |---|---|---| | TypeScript | `@plungeai/one-api` (npm) | `npm install @plungeai/one-api` | | Python | `plungeai` (PyPI; sync + async over httpx) | `pip install plungeai` | | Go | `github.com/PlungeAI/plungeai-go` (stdlib only) | `go get github.com/PlungeAI/plungeai-go` | | Rust | `plungeai` (crates.io; async over reqwest) | `cargo add plungeai` | Release status: `npm view @plungeai/one-api version`, `pip index versions plungeai`, `go list -m -versions github.com/PlungeAI/plungeai-go`, `cargo search plungeai`; until a registry answers, build the package from its directory under `sdk/`. ## TypeScript ```ts import PlungeAI from '@plungeai/one-api' const client = new PlungeAI() const me = await client.whoami() const run = await client.agents.execute('one-agent', { prompt: 'Reply with exactly the word: pong', max_tokens: 32 }) const hits = await client.discovery.search({ q: 'web search', kind: 'agents', limit: 3 }) const stream = await client.workflows.executeInlineStream({ workflow }) // workflow: CNL YAML string for await (const event of stream) console.log(event.event, event.data) for await (const w of client.workflows.list()) console.log(w) // walks every page try { await client.agents.execute('no-such-agent', { prompt: 'hi' }) } catch (err) { if (err instanceof PlungeAI.NotFoundError) console.log(err.status, err.code, err.requestID) else throw err } ``` Each status has its own error class (`BadRequestError` … `FailedDependencyError` for 424, `InternalServerError` for ≥ 500), all exported on `PlungeAI`; a per-request `idempotencyKey` is sent as `x-trace-id`, so a replayed run answers 409 `duplicate_execution_id` instead of starting twice. Zero runtime dependencies, ESM and CJS. ## Python ```python from plungeai import PlungeAI, NotFoundError client = PlungeAI() # or AsyncPlungeAI() — same methods, awaited me = client.whoami() run = client.agents.execute("one-agent", prompt="Reply with exactly the word: pong", max_tokens=32) hits = client.discovery.search(q="web search", kind="agents", limit=3) stream = client.workflows.execute_inline_stream(workflow=workflow) # workflow: CNL YAML string for event in stream: print(event.event, event.data) for w in client.workflows.list(): # walks every page print(w) try: client.agents.execute("no-such-agent", prompt="hi") except NotFoundError as err: print(err.status_code, err.code, err.request_id) ``` Method names are `snake_case` (`execute_inline_stream`); request fields are keyword arguments typed as `TypedDict`s; responses are plain `dict`s typed the same way. `stream=True` on `agents.execute` and `chat.completions.create` returns a `Stream`. Per-call options: `extra_headers`, `extra_query`, `timeout`, `max_retries`, `idempotency_key`; `client.agents.with_raw_response.execute(...)` gives the status, headers and request id. One runtime dependency: `httpx`. Python ≥ 3.10. ## Go ```go import ( "context" "errors" "fmt" "github.com/PlungeAI/plungeai-go" ) client := plungeai.NewClient() // plungeai.WithAPIKey(…) to override PLUNGEAI_API_KEY ctx := context.Background() me, err := client.Whoami(ctx) run, err := client.Agents.Execute(ctx, "one-agent", plungeai.AgentExecuteParams{Prompt: "Reply with exactly the word: pong", MaxTokens: plungeai.Int(32)}) hits, err := client.Discovery.Search(ctx, plungeai.DiscoverySearchParams{Q: "web search", Kind: "agents", Limit: plungeai.Int(3)}) stream, err := client.Workflows.ExecuteInlineStream(ctx, plungeai.WorkflowExecuteInlineStreamParams{Workflow: workflow}) for stream.Next() { ev := stream.Current() fmt.Println(ev.Event, ev.Data) } err = stream.Err() pager := client.Workflows.ListAutoPaging(ctx, plungeai.WorkflowListParams{}) // walks every page for pager.Next() { fmt.Println(pager.Current()) } _, err = client.Agents.Execute(ctx, "no-such-agent", plungeai.AgentExecuteParams{Prompt: "hi"}) var apiErr *plungeai.Error if errors.As(err, &apiErr) && apiErr.StatusCode == 404 { fmt.Println(apiErr.StatusCode, apiErr.Code, apiErr.RequestID) } ``` One package, no sub-packages: options are `plungeai.WithAPIKey`, `WithBaseURL`, `WithMaxRetries`, `WithRequestTimeout`, `WithHeader`, `WithQuery`, `WithIdempotencyKey`, `WithResponseInto`, usable on `NewClient` and on every call. Streaming variants of the `stream`-flag operations are `Agents.ExecuteStreaming` and `Chat.Completions.CreateStreaming`; list operations have a `…AutoPaging` twin. Errors are one type, `*plungeai.Error`, classified by `StatusCode`. Standard library only. Go ≥ 1.24. ## Rust ```rust use futures_util::StreamExt; use plungeai::{types::*, Client}; let client = Client::new()?; // or Client::builder().api_key(…).build()? let me = client.whoami().await?; let run = client .agents() .execute("one-agent", &AgentExecuteParams { prompt: Some("Reply with exactly the word: pong".into()), max_tokens: Some(32), ..Default::default() }) .await?; let hits = client.discovery().search(&DiscoverySearchParams { q: Some("web search".into()), kind: Some(DiscoverySearchParamsKind::Agents), limit: Some(3), ..Default::default() }).await?; let mut stream = client.workflows().execute_inline_stream(&WorkflowExecuteInlineStreamParams { workflow: workflow.into(), ..Default::default() }).await?; while let Some(event) = stream.next().await { let event = event?; println!("{:?} {}", event.event, event.data); } let mut items = client.workflows().list(&Default::default()).await?.into_stream(); // walks every page while let Some(w) = items.next().await { println!("{:?}", w?); } match client.agents().execute("no-such-agent", &AgentExecuteParams { prompt: Some("hi".into()), ..Default::default() }).await { Err(e) if e.is_not_found() => println!("{:?} {:?}", e.status(), e.request_id()), other => { other?; } } ``` Params and responses are `serde` structs under `plungeai::types` (optional fields `Option<T>`, response enums carry an `Unknown` variant so a new server value never breaks a client). Errors are one enum, `plungeai::Error`, with `status()` and `is_not_found()`-style predicates; streaming variants are `execute_stream`; pages expose `has_next_page()`, `next_page()` and `into_stream()`, carry `request_id` and `headers`, and have a `list_raw` form. Async over `reqwest` (rustls); `tokio` runtime. Rust ≥ 1.85. ## OpenAI-compatible frameworks (models plane) Any framework that accepts an OpenAI base URL works. Set base URL `https://api.plungeai.com/v1` and your `ozk_` key. LiteLLM: ```python import litellm resp = litellm.completion( model="openai/anthropic/claude-sonnet-4-6", # openai/ tells LiteLLM to use the OpenAI wire api_base="https://api.plungeai.com/v1", api_key="<your ozk_ key>", messages=[{"role": "user", "content": "pong?"}], ) ``` Vercel AI SDK: ```ts import { createOpenAI } from '@ai-sdk/openai' const ocean = createOpenAI({ baseURL: 'https://api.plungeai.com/v1', apiKey: process.env.PLUNGE_API_KEY, }) const model = ocean('anthropic/claude-sonnet-4-6') ``` LangChain (Python): ```python from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="anthropic/claude-sonnet-4-6", base_url="https://api.plungeai.com/v1", api_key="<your ozk_ key>", ) ``` ## MCP clients (the platform as an MCP server) The platform is itself an MCP server. Point any MCP client at the inbound Streamable-HTTP endpoint with your `ozk_` key. There are two equivalent URLs (both live-verified, both return the same tool table): `https://api.plungeai.com/v1/mcp` (the One API's pass-through, which forwards your key) and `https://mcp.plungeai.com/v1` (the MCP gateway directly). Claude Code: ```bash claude mcp add --transport http plungeai https://api.plungeai.com/v1/mcp \ --header "Authorization: Bearer $PLUNGE_API_KEY" ``` Claude Desktop / other clients that speak `mcp-remote`: ```json { "mcpServers": { "plungeai": { "command": "npx", "args": ["mcp-remote", "https://api.plungeai.com/v1/mcp", "--header", "Authorization: Bearer ${PLUNGE_API_KEY}"] } } } ``` You can confirm the tool table over plain curl (JSON-RPC `tools/list`) — live-verified: ```bash curl -s -X POST https://api.plungeai.com/v1/mcp \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` See [MCP plane](/one-api/mcp) for inbound tools and outbound MCP runs, and [Connecting clients](https://mcp.plungeai.com/docs#3-connecting-clients) in the MCP developer guide for every supported client's config and how to verify it. ## Ocean CLI (`ocean-cli`) A non-interactive terminal client for scripts, CI and other agents: `ocean-cli <noun> <verb>`, `--json` on every verb, stable exit codes. Package `@plungeai/ocean` (not yet on npm; run it from the repo with `npm run ocean-cli -- --help`). `ocean-cli login` stores your `ozk_` key; the Studio nouns also need `ocean-cli login --session`. Each noun rides one of three surfaces: | Surface | Credential | Nouns | |---|---|---| | One API (`https://api.plungeai.com/v1`) | `ozk_` key (`X-API-Key`; `Authorization: Bearer` on the models plane) | `search`, `extract`, `answer`, `research`, `findall`, `monitor`, `enrich` (through `POST /v1/tools/parallel-agent/execute`); `agents`; `tools list\|get\|call`; `workflows run` (waiting or `--stream`), `workflows result\|cancel`; `discovery`; `models`; `mcp` (outbound runs); `traces` | | MCP (`https://mcp.plungeai.com/v1`) | `ozk_` key (`Authorization: Bearer`) | `workflows list\|get\|create\|update\|delete\|versions\|restore\|build`, `workflows run --no-wait`; `executions`; `missions`; `chat`; `schedules`; `memory`; `learn`; `templates`; `tools call --async`; `whoami` | | Studio session (`https://studio.plungeai.com`, `https://dashboard.plungeai.com`) | `auth-session` cookie | `keys`, `balance`, `usage`, `nodes`, `campaigns`, `connections`, `secrets`, `documents`, `teams`, `projects`, `executions export` | `ocean-cli doctor` checks all three hosts and both credentials. Full command reference: `apps/ocean-cli/README.md`. ## curl conventions used in this guide - **Base URL** is always `https://api.plungeai.com`. - **Auth** is `-H "Authorization: Bearer $PLUNGE_API_KEY"` on every plane; `-H "X-API-Key: $PLUNGE_API_KEY"` also works on the execution planes. - **JSON bodies** use `-H "Content-Type: application/json"`; workflow YAML can be posted with `-H "Content-Type: text/yaml" --data-binary @workflow.yaml`. - **Streaming** examples add `-N` so curl does not buffer the SSE. - **Correlation:** add `-H "x-trace-id: <a UUID>"` to tie several calls into one trace. A plain execution-plane list call (the shape every `ozk_` GET follows) is live-verified: ```bash curl -s "https://api.plungeai.com/v1/agents?limit=3" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` # Bots Source: https://www.plungeai.com/docs/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](/one-api/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](/one-api/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](/one-api/errors-limits)). | Section | Routes | |---|---| | [16.1 Lifecycle](#161-lifecycle) | `/v1/workflows`, `/v1/workflows/{id}/versions` | | [16.2 Policy and budget](#162-policy-and-budget) | `/v1/workflows/{id}/permissions` | | [16.3 Executions and approvals](#163-executions-and-approvals) | `/v1/executions` | | [16.4 Schedules and wake](#164-schedules-and-wake) | `/v1/schedules` | | [16.5 Triggers and hooks](#165-triggers-and-hooks) | `/v1/triggers` | | [16.6 Workspace, inbox, bindings, groups](#166-workspace-inbox-bindings-groups) | `/v1/workflows/{id}/workspace-files`, `/inbox`, `/bindings`, `/v1/bot-groups`, `/v1/channels` | | [16.7 Memory and skills](#167-memory-and-skills) | `/v1/memory`, `/v1/skills` | | [16.8 Templates](#168-templates) | `/v1/templates` | | [16.9 The one door](#169-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](/one-api/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. ```bash 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: ```bash curl -s "https://api.plungeai.com/v1/workflows?kind=bot&limit=3" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ```bash 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](#169-the-one-door)), 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 …]`). ```bash 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](#162-policy-and-budget)); 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`. ```bash curl -s "https://api.plungeai.com/v1/executions?limit=3" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ```bash 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: ```bash 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: ```bash 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: ```bash 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 ```bash 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`. ```bash 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](#163-executions-and-approvals)). 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. ```bash 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 ```bash 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`. ### 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 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`. ```bash curl -s https://api.plungeai.com/v1/bot-groups \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ```bash 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](/one-api/skills-plugins-capabilities)). | 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. ```bash curl -s "https://api.plungeai.com/v1/memory/recall?limit=1" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` ```bash 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](#161-lifecycle): 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`. ```bash 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](/one-api)): the body is the program, and the call **always runs through the engine**, so a saved bot keeps its stored policy ([16.2](#162-policy-and-budget)). 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: 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](/one-api/connectors#73-how-your-testers-connect-an-account)); 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 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](/one-api/connectors#72-how-a-connected-account-flows-into-an-api-call)) | ```bash 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`): ```bash 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`. # Companies Source: https://www.plungeai.com/docs/one-api/companies A **company** is a campus. You describe it in YAML documents, save them like any workflow, and **apply** them. The platform compiles them into a plan and runs it: it puts **items** (a feature, a brief, a piece of content) through each building's offices in order, and it stops at **gates** where a person decides. This chapter is the REST surface for all of it. Base URL is `https://api.plungeai.com`; every route takes an `ozk_` key ([Authentication](/one-api/authentication)). The same rules as [Bots](/one-api/bots) hold on every route here: - **Your rows only.** The key's account is the tenant. A campus another account owns answers the same `404 company_not_found` as one that does not exist. - **Four wires.** JSON by default. `POST`/`PUT` bodies may be YAML, and `format=` or `Accept` picks the response format (`json`, `yaml`, `markdown`, `text`). The event feed also streams as server-sent events. - **One error shape.** `{"error": {"code", "message"}}` plus the `X-Error-Code` header ([Errors, limits & headers](/one-api/errors-limits)). | Method and path | What it does | |---|---| | `GET /v1/companies` | your applied campuses: mode, status, building count, approvals waiting | | `POST /v1/companies/{campus}/apply` | compile the saved documents into the stored plan | | `GET /v1/companies/{campus}/layout` | the compiled company: buildings, policy, floors, intake, pipeline, offices | | `GET /v1/companies/{campus}/snapshot` | the company now: office states, active items, the line at the door, spend today | | `GET /v1/companies/{campus}/events` | the event log: a JSON page after `since`, or a live SSE feed (`Accept: text/event-stream` or `?stream=1`) | | `POST /v1/companies/{campus}/events` | a local building's node reports its events up (`{events: [...]}`, 1 to 200) | | `GET /v1/companies/{campus}/buildings/{building}/slice` | what a local building's node pulls to run it | | `POST /v1/companies/{campus}/items` | put an item into a building: `{building, title, body?, kind?, priority?, tier?}` → `201` | | `POST /v1/companies/{campus}/items/{id}/pause` · `resume` · `retry` · `cancel` · `steer` | act on one item (`steer` takes `{text}`) | | `GET /v1/companies/{campus}/approvals` | the door: pending gates and tool permissions (`status=pending\|approved\|rejected\|expired`) | | `POST /v1/companies/{campus}/approvals/{ap}` | decide: `{decision: approve\|reject, note?}` | | `PUT /v1/companies/{campus}/mode` | `{mode: supervised\|tiered\|autonomous}`, live | | `POST /v1/companies/{campus}/start` · `stop` | arm or disarm the campus clock; both idempotent | | `DELETE /v1/companies/{campus}` | delete a stopped or never-started company and every row of it → `204` | Source: `orchestration/api-gateway/routes/companies.ts` (the wire) over `orchestration/company-service/` (every rule; `contract.ts` is its surface, `README.md` its grammar). Every response below comes from a live walk on 2026-10-14 (UTC) against a throwaway campus that was deleted at the end. Ids are shown as `…`. ## 17.1 What a campus is | Part | What it is | |---|---| | **Campus** | a document of `kind: campus`: a name, a slug, a mode and its buildings in map order | | **Building** | a `kind: building` document, or a building written inline in the campus. It has floors of offices, a **standard** its policy follows, the intake items come through, and the **pipeline** an item walks | | **Office** | an ordinary CNL workflow: the agents doing the work. It is a saved workflow name, or inline `{name, workflow}`. An inline office is saved for you on apply as `<campus>/<building>/<office>` | | **Item** | one piece of work put into a building. It walks the pipeline step by step | | **Gate** | a pipeline step where the standard's policy and the mode decide: pass it (`auto`) or ask a person (an **approval**) | `location: cloud` buildings run their offices on the platform once the campus clock is started. `location: local` buildings run on your own machine: the node pulls the building's **slice** and reports its events up. The cloud clock never dispatches a local building's items. The **mode** decides who passes a gate. `supervised` asks a person at every gate. `tiered` asks according to the standard's tier table. `autonomous` asks only where a building's policy says `require`. There are two standards. `code` (the software factory) has the gates `spec`, `build_permission`, `diff`, `merge` and `deploy`. `action` (the venture studio) has the gates `brief`, `tool_permission`, `content`, `commitment` and `release` (`orchestration/company-service/rubrics/`). ## 17.2 The documents: kinds `campus` and `building` Documents are compiled, never executed. Save them with `POST /v1/workflows` and `kind: campus` or `kind: building` ([16.1](/one-api/bots#161-lifecycle)). The walk below used one campus document with its building inline: a cloud building whose pipeline starts with a gate, so nothing ran and nothing was spent. ```yaml kind: campus campus: name: plan-docs-… slug: plan-docs-… mode: supervised buildings: - name: Desk location: cloud policy: tiers: action floors: - name: "1" offices: - name: draft workflow: description: "Desk: draft one line from the item." tasks: - type: harness id: writer agent: one-agent goal: "Draft one line from this item. {input}" mission: "You are the desk's writer." allowed_tools: [task_complete] max_iterations: 2 intake: - api: true pipeline: - gate: brief - office: draft ``` | Key | Level | Meaning | |---|---|---| | `campus.name` · `campus.slug` | campus | the slug is the `{campus}` of every route (`^[a-z0-9][a-z0-9-]{0,62}$`). It defaults to the slugified name | | `campus.buildings[]` | campus | building slugs in map order (each a `kind: building` document), or inline building bodies | | `mode` · `manager` | campus, building | who decides at the doors (`user:<id>` or `team:<id>`); a building overrides the campus | | `building.campus` | building document | the campus slug it stands on; it must equal the campus that lists it | | `location` · `project` | building | `cloud` or `local`; `project` is for local buildings only | | `policy.tiers` · `policy.gates` · `policy.budget` | building | the standard (`code` or `action`), a per-gate `require` / `auto` / `tiered`, and `day_usd` / `item_usd` hard stops | | `floors[].offices[]` | building | office names (saved workflows, runner words in a local building, or inline `{name, workflow}`) | | `intake[]` | building | `{api: true}` (the items route), `{bot: <saved bot>}`, `{schedule: <cron>, office: <office>}` | | `pipeline[]` | building | in order: `{office}`, `{type: parallel\|sequential, id, offices}`, `{gate}`, `{then: <building>}` (last only) | A document may not name machines or credentials: `node`, `path`, `bindings`, `api_key`, `token`, `secret`, `service`, `worker`, `url`, `endpoint` and the like are refused, and an unknown key is refused with its path. The full grammar is `orchestration/company-service/README.md`. ```bash curl -s -X POST https://api.plungeai.com/v1/workflows \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d "$(jq -n --arg y "$(cat campus.yaml)" '{name:"plan-docs-…", kind:"campus", yaml:$y}')" # → 201 {"workflow":{"id":"…","name":"plan-docs-…","kind":"campus","description":null,"folder":null,"updated_at":…,"version":null}} ``` ## 17.3 Apply Apply reads your saved documents, compiles them and stores the plan. It also saves any inline office as a workflow. Apply again after you edit a document; an inline office the new plan no longer names is deleted. ```bash curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/apply \ -H "Authorization: Bearer $PLUNGE_API_KEY" # → 200 {"campus":"plan-docs-…","buildings":[{"slug":"desk","location":"cloud","offices":1}], # "workflows":[{"name":"plan-docs-…/desk/draft","id":"…","action":"created"}], # "schedules":[],"local":[],"warnings":[]} ``` A document set that does not compile is `422 compile_failed`, with every `path: message` in the message. No saved campus document with that slug is `404 company_not_found`. An inline office whose saved name (`<campus>/<building>/<office>`) is already taken by an agent or a bot is `409 office_conflict` (`orchestration/company-service/docs.ts`). List and layout are read-only: ```bash curl -s https://api.plungeai.com/v1/companies -H "Authorization: Bearer $PLUNGE_API_KEY" # → 200 {"companies":[…,{"slug":"plan-docs-…","name":"plan-docs-…","mode":"supervised","status":"applied", # "applied_at":"2026-10-14T00:29:15.962Z","buildings":1,"pending":0},…]} ``` ```bash curl -s https://api.plungeai.com/v1/companies/plan-docs-…/layout -H "Authorization: Bearer $PLUNGE_API_KEY" # → 200 {"campus":{"slug":"plan-docs-…","mode":"supervised","manager":null,…}, # "buildings":[{"slug":"desk","location":"cloud","mode":"supervised", # "policy":{"rubric":"action","gates":{"brief":"tiered","tool_permission":"tiered","content":"tiered", # "commitment":"tiered","release":"tiered"},"budget":{"day_usd":null,"item_usd":null}}, # "intake":[{"api":true}], # "pipeline":[{"id":"gate:brief","kind":"gate","gate":"brief"}, # {"id":"draft","kind":"office","offices":["draft"],"retry":null,"skip":null}], # "connected":true,…}], # "offices":[{"building":"desk","slug":"draft","name":"plan-docs-…/desk/draft","floor":"1","order":0,"source":"inline"}]} ``` ## 17.4 Start ```bash curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/start -H "Authorization: Bearer $PLUNGE_API_KEY" # → 200 {"status":"running"} ``` Once the clock is started, cloud buildings walk their items. An office step starts an engine run, which is paid like any other run. `stop` disarms the clock; items keep their step and wait (17.7). ## 17.5 Items ```bash curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/items \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"building":"desk","title":"Launch note","body":"One line for the launch.","kind":"content"}' # → 201 {"id":"…","campus":"plan-docs-…","building":"desk","title":"Launch note","body":"One line for the launch.", # "kind":"content","source":"api","priority":0,"tier":null,"step":"intake","attempt":0,"status":"active", # "parked_reason":null,"paused":0,"steer":null,"last_output":null,"cost_usd":0,"parent_item_id":null,…} ``` | Field | Rule | |---|---| | `building` | a building slug of this campus (required) | | `title` | 1 to 300 characters (required) | | `body` | text, up to 65,536 characters | | `kind` | `feature`, `bugfix`, `docs`, `research`, `content` or `task` | | `priority` | a whole number | | `tier` | a tier word of the standard (`T0`…`T3`, `V0`…`V3`), up to 32 characters; without it the standard's default | Any other field is `422 invalid_params` (`unknown field: …`). The snapshot shows where the item is. In the walk below it is waiting at the gate, and its approval is in the `queue`: ```bash curl -s https://api.plungeai.com/v1/companies/plan-docs-…/snapshot -H "Authorization: Bearer $PLUNGE_API_KEY" # → 200 {"head_seq":4,"mode":"supervised","status":"running", # "buildings":[{"slug":"desk","location":"cloud","connected":true,"mode":"supervised","active":1}], # "offices":[{"building":"desk","office":"draft","state":"idle","item_id":null}], # "items":[{"id":"…","title":"Launch note","step":"gate:brief","attempt":0,"status":"active",…}], # "queue":[{"id":"…","item_id":"…","step":"gate:brief","gate":"brief","tier":"V2","status":"pending",…}], # "truncated":false,"spend_today_usd":0,"shipped_7d":0} ``` The five item actions are `POST …/items/{id}/<action>`. Each answers the item as it is now: | Action | Effect | |---|---| | `pause` | the item keeps its step and is not walked until resumed | | `resume` | a paused or parked item goes on at the step it stopped on (a parked item starts a fresh visit) | | `retry` | the step starts again as `attempt + 1`, or at the pipeline's retry target | | `cancel` | the item ends `failed` with `parked_reason: cancelled`; running office runs are not interrupted | | `steer` | `{text}` (1 to 2,000 characters after trimming): a note the next office run receives once. A blank or longer `text` is `422 invalid_params` | A cancelled item (`status: failed`) refuses `pause` and `resume` with `409 item_failed`; `retry` it instead. Any action on a finished item is `409 item_done` (`orchestration/company-service/company-service.ts`). This walk did not call the five actions. The live oracle `tests/live/companies/items-actions.mjs` runs all five against the deployed API on a local building of its own. ## 17.6 Approvals: the door When an item reaches a gate that asks a person, the gate opens an approval. In `supervised` mode every gate asks. ```bash curl -s "https://api.plungeai.com/v1/companies/plan-docs-…/approvals?status=pending" \ -H "Authorization: Bearer $PLUNGE_API_KEY" # → 200 {"approvals":[{"id":"…","item_id":"…","building":"desk","step":"gate:brief","gate":"brief","tier":"V2", # "status":"pending","payload_json":"{\"summary\":\"brief: Launch note\"}","execution_id":null, # "requested_at":"2026-10-14T00:29:18.611Z","expires_at":"2026-10-16T00:29:18.304Z", # "decided_at":null,"decided_by":null,"note":null}]} ``` ```bash curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/approvals/… \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"decision":"reject","note":"Docs walk: nothing to run."}' # → 200 {"approval":{"id":"…","item_id":"…","step":"gate:brief","gate":"brief","tier":"V2","status":"rejected", # "execution_id":null,"decided_at":"2026-10-14T00:29:19.573Z","decided_by":"user:…", # "note":"Docs walk: nothing to run.",…}} ``` `approve` moves the item on to the next step, and `reject` parks it (`parked_reason: rejected`). A gate approval has `execution_id: null`. A **tool permission** is different: an office run paused to ask for a tool. Approving it also continues that execution once and hands its result back to the item. The answer then adds `execution_id` and `continued: true`, or `continued: false` with a `continue_error` (`not_paused`, `session_expired`, `execution_not_found`, `continue_failed`). The decision stands either way. Rejecting a tool permission leaves the run paused. A body other than `{decision: approve|reject, note?}` is `422 invalid_params`. ## 17.7 Stop and delete `DELETE` refuses a running company: ```bash curl -s -X DELETE https://api.plungeai.com/v1/companies/plan-docs-… -H "Authorization: Bearer $PLUNGE_API_KEY" # → 409 {"error":{"code":"conflict","message":"company \"plan-docs-…\" is running — stop it first"}} ``` ```bash curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/stop -H "Authorization: Bearer $PLUNGE_API_KEY" # → 200 {"status":"stopped"} curl -s -o /dev/null -w '%{http_code}\n' -X DELETE https://api.plungeai.com/v1/companies/plan-docs-… \ -H "Authorization: Bearer $PLUNGE_API_KEY" # → 204 curl -s https://api.plungeai.com/v1/companies/plan-docs-…/snapshot -H "Authorization: Bearer $PLUNGE_API_KEY" # → 404 {"error":{"code":"company_not_found","message":"no company \"plan-docs-…\""}} ``` Delete removes every company row at once: buildings, offices, items, runs, approvals, the event log and the manifest. It does **not** remove your saved documents (the campus document and the inline offices saved as `<campus>/<building>/<office>`). Find them with `GET /v1/workflows?search=<slug>` and delete each one with `DELETE /v1/workflows/{id}` → `204` ([16.1](/one-api/bots#161-lifecycle)). The walk did this for both of its documents, and afterwards the search listed none and `GET /v1/companies` no longer listed the campus. ## 17.8 Mode, events and local buildings `PUT …/mode` takes `{mode}` and switches the whole campus live, from the next tick. It overrides the campus and every building, a building's documented mode included, and it survives apply. Setting the campus's documented mode clears the override. A missing or unknown mode is `422 invalid_params`. `GET …/events` answers a JSON page of the log after `since` (a seq). With `Accept: text/event-stream` or `?stream=1` it is a live feed of `id: <seq>`, `event: <type>`, `data: <row>`. A local building's node uses two more routes with the owner's key: `GET …/buildings/{building}/slice`, which returns the building's compiled pipeline, policy, project and office ids, and `POST …/events` (`{events: [{building, type, item_id?, office?, data?, at?}]}`, 1 to 200 events) to report back. This walk did not call these four routes. Their rules are in `routes/companies.ts` and `routes/companies-events.ts`. ## 17.9 Errors | Status | Code | When | |---|---|---| | 400 | `invalid_format` | `format=` is not `json`, `yaml`, `markdown` or `text` | | 404 | `company_not_found` | no campus with that slug for this key, or a slug that cannot be one (answered without a lookup) | | 404 | `building_not_found` · `item_not_found` · `approval_not_found` | no such building, item or approval in this company | | 409 | `conflict` | `DELETE` while the company runs: stop it first | | 409 | `item_failed` · `item_done` | `pause` or `resume` of a cancelled item (retry it); any action on a finished item | | 409 | `approval_not_pending` | deciding an approval that is already decided or expired | | 409 | `office_conflict` | apply: an inline office's saved name is taken by an agent or a bot | | 422 | `invalid_params` | an item, decision, mode, steer or events body that breaks its rule; the message names the field | | 422 | `compile_failed` | apply: the documents do not compile; the message lists each `path: message` | | 503 | `not_enabled` | the deployment has no company service | ```bash curl -s https://api.plungeai.com/v1/companies/no-such-campus/layout -H "Authorization: Bearer $PLUNGE_API_KEY" # → 404 {"error":{"code":"company_not_found","message":"no company \"no-such-campus\""}} ``` ```bash curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/items \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' -d '{"building":"desk"}' # → 422 {"error":{"code":"invalid_params","message":"title must be 1-300 characters"}} ``` The same operations are in the API reference under **Companies**, one page per route. # MCP guide Source: https://www.plungeai.com/docs/mcp Source: `orchestration/mcp-gateway/index.ts`, `server.ts`, `auth-middleware.ts`, `execution.ts`, `prompts.ts`, `resources.ts`, `docs-routes.ts`, `orchestration/api-gateway/routes/mcp.ts`, `orchestration/api-gateway/lib/auth.ts`; live `GET /health`, `GET /v1/info` and unauthenticated `POST /v1` (live-verified 2026-09-24). The **PlungeAI MCP server** puts the PlungeAI platform behind the Model Context Protocol. You connect an MCP client (Claude Code, Cursor, VS Code, Codex, Lovable, Replit, …) once, then ask for things in plain language. The client picks one of the server's 20 `plungeai_*` tools, fills in the arguments, and the platform does the work: it searches the agent registry, runs agents, CNL workflows and bounded missions, schedules jobs, keeps memory, and returns the finished result as Markdown. https://mcp.plungeai.com/v1 It is one server with one catalog. The same server is also reachable through the One API at `https://api.plungeai.com/v1/mcp`; the differences are listed in [0.2](#02-the-two-endpoints). ## 0.1 Start here — one call Get an `ozk_` key at Dashboard → **One API → Keys** (`https://dashboard.plungeai.com`), export it as `PLUNGE_API_KEY`, then ask the server who you are: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I"}}}' ``` The answer is a server-sent event whose `data:` line carries your identity card (user id, tier, key label, rate-limit window). Connecting a real client takes one command — see the [Quickstart](/mcp/quickstart). ## 0.2 The two endpoints Both URLs reach the same server code, the same 22 tools, the same prompt and the same resources. Use whichever your client makes easiest; `https://mcp.plungeai.com/v1` is the canonical one and the one this guide uses. | | `https://mcp.plungeai.com/v1` | `https://api.plungeai.com/v1/mcp` | |---|---|---| | What it is | The MCP gateway itself | A One API route that forwards each request to the gateway together with your own key | | Methods | `POST` (JSON-RPC). `GET`/`DELETE` without a session → `405` | `POST` only | | Credentials | `Authorization: Bearer ozk_…` or `X-API-Key: ozk_…` | the same two headers; the value must start with `ozk_` | | `401` body | JSON-RPC: `{"jsonrpc":"2.0","error":{"code":-32001,"message":"No valid authentication provided. …"},"id":null}` plus `WWW-Authenticate: Bearer` | One API envelope: `{"error":{"code":"unauthorized","message":"Bearer ozk_ key required"}}` plus `X-Error-Code: unauthorized` | | What counts toward your rate limit | `tools/call` only | `tools/call` only, counted by the gateway | | `plungeai_whoami` shows | `api-key`, your key's tier and label | the same (the One API forwards your key) | | Extra routes | public install, docs and skill routes (below) | `GET /v1/mcp/tools` (a plain `GET` that returns the server's `tools/list` answer as the JSON-RPC envelope `{"jsonrpc":"2.0","id":1,"result":{"tools":[…]}}`, without you sending JSON-RPC) and the outbound MCP runs — see `https://api.plungeai.com/docs#11-mcp-plane` | Two more facts about the canonical host: - The pre-2026-09-20 path `https://mcp.plungeai.com/mcp` still answers with the same handler, so client configs that point at it keep working. New configs use `/v1`. - The server answers only on its own hostname. A request that arrives with any other `Host` header (for example from a reverse proxy that rewrites it) is refused with `403 Invalid Host header: <host>`. Public routes on `https://mcp.plungeai.com` (no key needed): | Route | What it returns | |---|---| | `GET /health` | dependency probes (`d1`, `kv_keys`, `registry`), server version, protocol; `200 healthy` or `503 degraded` | | `GET /v1/info` | server name `plungeai.com`, version, protocol version `2025-11-25`, capabilities | | `GET /install`, `/install.json`, `/install/:tool` | the one-click install page and its data — [3.15](/mcp/connecting-clients#315-one-click-installs) | | `GET /connect/<tool>/SKILL.md`, `/lovable/SETUP.md` | per-client setup skills — [3.15](/mcp/connecting-clients#315-one-click-installs) | | `GET /llms.txt`, `/llms-full.txt`, `/skill.zip`, `/skill/<file>` | the CNL workflow-authoring skill for LLM tools | | `GET /docs`, `/docs.md` | this guide, as a web page and as one Markdown file (paste the `.md` URL into an AI client) | `GET /log` (your own activity trail) needs your key — see [Errors, feedback & limits](/mcp/errors-limits). ## 0.3 Who it is for - **People working in an AI client** — a coding agent (Claude Code, Cursor, VS Code, Codex, Gemini CLI, OpenCode, Windsurf) or a chat app (Claude Desktop). You ask; the client calls the tools. - **App builders** — Lovable, Bolt, v0 and Replit connect the server to their builder chat, so the builder can discover and test PlungeAI agents while it writes your app. The app it ships calls the One API, not MCP ([Connecting clients](/mcp/connecting-clients) covers the split per builder). - **Developers** who want a JSON-RPC surface over the whole platform with structured outcomes, approvals and continuations — plain `curl` works, no SDK needed ([Quickstart](/mcp/quickstart)). ## 0.4 Tool families `tools/list` returns 22 tools. You rarely name them yourself: your client reads their descriptions and chooses. This is the map of what the server can do and where each tool is documented. | Family | Tools | Chapter | |---|---|---| | Discover | `plungeai_list_agents` (search the registry), `plungeai_get_tool_contract` (one agent's exact contract), `plungeai_templates` list/get | [Discovery](/mcp/discovery) | | Run a tool | `plungeai_execute_tool` (one structured tool-agent, typed params, behind the trust fence) | [Tools](/mcp/tools) | | Run an agent | `plungeai_execute_agent` (one prompt-driven agent), `plungeai_run_mission` (bounded autonomous mission) | [Agents & missions](/mcp/agents-missions) | | Workflows | `plungeai_list_workflows`, `plungeai_execute_workflow`, `plungeai_workflow` (create / update / versions), `plungeai_build_workflow`, `plungeai_templates` use | [Workflows](/mcp/workflows) | | Runs and results | `plungeai_get_result`, `plungeai_get_workflow_status`, `plungeai_executions`, `plungeai_followup`, `plungeai_continue` | [Runs, results & follow-ups](/mcp/runs-results) | | Chat, memory, skills | `plungeai_chat`, `plungeai_memory`, `plungeai_learn` | [Chat, memory & skills](/mcp/chat-memory-skills) | | Scheduling | `plungeai_schedule` | [Scheduling](/mcp/scheduling) | | Identity | `plungeai_whoami` | [Authentication & keys](/mcp/authentication) | | Companies | `plungeai_companies` (campuses compiled from your saved documents: items, gates, the clock) | [Companies](/mcp/companies) | | Connections | `plungeai_connections` (your connected accounts and your customers': list, add a key, remove, send a connect link) | [Connections](/mcp/connections) | Besides tools, the server offers one prompt (`plungeai`; in Claude Code it is `/plungeai:plungeai (MCP)` in the slash menu) and five `plungeai://` resources plus three resource templates — see [Prompts & resources](/mcp/prompts-resources). Every tool's parameters are in the [Tool reference](/mcp/tool-reference). Two words you will meet everywhere: - A **registry agent** is a platform building block in the catalog (`brave-agent`, `sec-agent`, `weather-agent`, …). About 90 are active; `plungeai_list_agents` returns the live set. - **Your workflows** are your saved CNL workflows. In everyday speech users call these "my agents": "show me my agents" means your workflows (`plungeai_list_workflows`), not the registry. Say "agents in the registry" or "what can the platform do" when you mean building blocks. The server tells every client this at connect time. ## 0.5 How a request flows 1. Your client sends a JSON-RPC message with `POST` to `/v1`. Each request stands alone: no session id is needed (sessions are opt-in per key — [Protocol & transport](/mcp/protocol)). 2. The edge checks run in this order (`index.ts`): method (`GET`/`DELETE` → `405`), `Host` (→ `403`), a declared body over 1 MiB (→ `413`; the body is also capped while it is read), then your key (→ `401`; failed attempts are metered per IP). A `tools/call` is then counted against your rate limit (→ `429`), checked against the tool catalog and your key's tool fence (→ JSON-RPC `-32602`), and its arguments are checked against the tool's schema — bad arguments come back as a `needs_input` answer and nothing runs. 3. A fresh server instance is built for the request with only the tools your key may use. Cheap methods (`initialize`, `tools/list`, `resources/*`, `prompts/*`) answer with plain JSON. 4. The tool runs. Discovery tools read the live registry. Execution tools refuse any agent that is not `status:active` before dispatch, then hand the work to the CNL engine over an internal service binding; the engine runs the agents (in parallel where the workflow says so) and stores the result. 5. A `tools/call` answers as a server-sent-event stream: live progress notifications while it works, then one final result. The result is platform-rendered Markdown by default, or JSON with `structuredContent` when you pass `format: "json"`. Execution tools answer with a structured **outcome** (`ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error`), never a bare error ([Errors, feedback & limits](/mcp/errors-limits)). 6. Every authenticated request is written to your per-user activity trail, and every run is a row in the same execution history that Ocean Studio and the dashboard read. Through `https://api.plungeai.com/v1/mcp`, the One API authenticates your `ozk_` key then forwards it to the same gateway, which validates it again and applies the key's restrictions and rate limit; steps 3–6 are identical. ## 0.6 What using it feels like **One account, everywhere.** A workflow you save here is the same workflow you see in Studio. There is no separate "MCP workspace": workflows, runs and chat conversations you create through MCP appear in the other PlungeAI apps, and the other way round. Everything you can see or run is scoped to the account behind your key ([2.8](/mcp/authentication#28-identity-and-scoping)). **You don't name tools.** Describe what you want ("find my due-diligence workflow and run it for Adobe"). The client chooses between the 22 tools and fills in the arguments. The server's connect-time instructions tell clients to discover agent ids with a live search rather than from memory, to relay results verbatim, and to never approve a paused action on their own. **Nothing runs silently.** While a tool works, the server streams status lines such as: ``` ⚙️ Agents working — 5/18 agents complete · ✓ brave-agent · 13 active (tavily-agent, exa-agent…) ✍️ Writing (3516 chars) — …the agent's answer as it is produced ✅ Complete — 18 agents in 24.6s ``` A long, quiet step sends a heartbeat every 15 seconds (`⏳ Working — 45s elapsed`). The feed goes out on the MCP progress channel when your client sends a `progressToken`, and on the logging channel when it does not, so any client can receive it. Whether it is shown is up to the client: some render it under the running tool, some ignore it. The work and the result are identical either way. Runs started with `mode: "async"` return an execution id at once and finish in the background, so they have no live feed — poll `plungeai_get_workflow_status` instead. Details: [Protocol & transport](/mcp/protocol). **Clients read the tool list once.** Most clients call `tools/list` when they connect. If a tool you expect is missing after a platform update, restart or reconnect the client ([3.1](/mcp/connecting-clients#31-before-you-start)). ## 0.7 MCP, the One API, skills — which one? All of them use the same `ozk_` key, the same account and the same data. | Surface | What it is | Use it when | |---|---|---| | **MCP server** — `https://mcp.plungeai.com/v1` | the 22 `plungeai_*` tools over MCP | an AI client operates PlungeAI live: discover, run, save, schedule, remember, with approvals and continuations | | **One API** — `https://api.plungeai.com` | REST routes, OpenAPI at `GET /v1/openapi.json` | you write or generate code that calls PlungeAI (apps, scripts, backends, CI) — `https://api.plungeai.com/docs` | | **Skills** — `https://skills.plungeai.com` | 32 instruction packs your AI client loads (setup per client, capability guides, CNL authoring) | you want your client to already know PlungeAI's conventions; skills complement a connection, they are not one — [3.16](/mcp/connecting-clients#316-the-skills-catalog) | | **Ocean Studio** — `https://studio.plungeai.com` | the browser app | a human builds visually, connects accounts (Connectors), saves bring-your-own keys, reviews runs | A common split, used by the app builders: the builder's AI uses MCP to find and test the right agent, then writes app code that calls the One API with a server-side key. ## 0.8 What "live-verified" means in this guide Examples that carry a hidden `# verify` / `# expect:` marker are read-only and free (`initialize`, `tools/list`, `plungeai_whoami`, the list/get/contract tools, public routes). The docs build strips the markers; `node orchestration/api-gateway/docs/guide-3.0/verify-examples.mjs <chapter.md>` re-runs them with `PLUNGE_API_KEY`. The unauthenticated behaviour (401 shapes, `/health`, `/install*`, `/connect/*`, the skills catalog) was run live on 2026-09-24. Examples that need a key were checked against the server code on 2026-09-24; examples that run a model, spend credits or change state carry no marker and say so in the text. ## 0.9 Where to get help - **This guide:** `https://www.plungeai.com/docs/mcp`, one page per chapter (Markdown: `https://mcp.plungeai.com/docs.md`; the old address `https://mcp.plungeai.com/docs` answers 301 here). - **Health, no key:** `https://mcp.plungeai.com/health`. - **One API guide:** `https://www.plungeai.com/docs/one-api`. **Skills catalog:** `https://www.plungeai.com/docs/skills`. **Install page:** `https://www.plungeai.com/install`. - **Your own request history:** `https://mcp.plungeai.com/log?date=YYYY-MM-DD` with your key ([Errors, feedback & limits](/mcp/errors-limits)). A tool that fails on a platform fault answers with a request id — include it when you report a problem. - **Keys:** self-service at Dashboard → One API → Keys. Support: [Requests](https://www.plungeai.com/request/new?kind=question). ## 0.10 Versions and what changed **How the server is versioned.** The version is the build's `serverInfo.version` in the `initialize` result (`SERVER_VERSION` in `server.ts`, `2.7.0` on 2026-10-13; the same value is `server.version` on `GET /v1/info` and `version` on `GET /health`). The tool surface itself is the `tools/list` catalog. Tools and parameters have no version of their own: they change in place when the server is deployed, and the stateless server sends no `list_changed` notice ([4.5](/mcp/protocol#45-stateless-by-default-opt-in-sessions)). When the version changes, re-read `tools/list` (reconnect your client). **This guide** replaces the earlier single-page MCP user guide, at the same URL (`https://mcp.plungeai.com/docs`). Notable corrections against that page: - **claude.ai (web) has no live tools.** The `mcp-remote` bridge is for Claude Desktop; claude.ai gets a skill upload only ([3.3](/mcp/connecting-clients#33-claudeai-and-claude-desktop)). - **Skills.** The per-client skills are `plungeai-in-<tool>` (the old `--skill plungeai-<tool>` names never existed). The skills `plungeai`, `plungeai-api`, `plungeai-cli`, `plungeai-connect` and `plungeai-mcp` were replaced by the 32-skill catalog (`plungeai-mcp-setup`, `plungeai-api-setup`, `plungeai-cli-setup`, …). `/connect/api/SKILL.md` answers `404`; REST setup is `plungeai-api-setup`. The `PlungeAI/plungeai-agent-skills` repository is not public yet, so the zips are the working install ([3.15](/mcp/connecting-clients#315-one-click-installs), [3.16](/mcp/connecting-clients#316-the-skills-catalog)). - **Ocean CLI.** The terminal client is `ocean-cli` (package `@plungeai/ocean`). `npm i -g @plungeai/ocean` does not work yet: the package is not published ([3.17](/mcp/connecting-clients#317-the-ocean-cli)). - **Protocol.** The server implements `2025-11-25`; the old raw-HTTP example asked for `2025-06-18`, which still negotiates ([4.3](/mcp/protocol#43-protocol-version-and-initialize)). - **Approvals.** Only an agent's staged action (`⏸ AWAITING USER APPROVAL`) continues with `plungeai_continue`. A trust-fence `⏸ NEEDS APPROVAL` cannot be continued over MCP; it runs only from Ocean Studio ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)). - **`provider: google`** on a `one-agent` task is accepted, not refused: it maps to `gemini`. Pass `model` explicitly, because the default model applies only when `provider` is omitted or `gemini` ([8.3](/mcp/workflows#83-ad-hoc-cnl-yaml)). - **Tool behaviour.** `plungeai_list_workflows` `search` matches workflow names only, not descriptions ([8.1](/mcp/workflows#81-find-your-workflows--plungeai_list_workflows)), and the `plungeai_chat` assistant cannot save a workflow: ask for `plungeai_build_workflow` ([10.1](/mcp/chat-memory-skills#101-chat-plungeai_chat)). - **Key fences.** Keys you create in the Dashboard carry no fences; `allowed_ips` and `allowed_tools` are set by PlungeAI ([2.5](/mcp/authentication#25-key-fences)). # Quickstart Source: https://www.plungeai.com/docs/mcp/quickstart Source: `orchestration/mcp-gateway/index.ts`, `server.ts`, `extras.ts` (`plungeai_whoami`), `tools.ts` (`listAgents`, `executeAgent`), `tool-outcome.ts`, `prompts.ts`, `integrations.ts`; `apps/ocean-skills/skills/plungeai-in-claude-code/SKILL.md`; MCP SDK `webStandardStreamableHttp.js` (SSE framing). Unauthenticated responses live-verified 2026-09-24. Five minutes from nothing to a real run. The endpoint is `https://mcp.plungeai.com/v1`; `https://api.plungeai.com/v1/mcp` is the same server if you prefer the One API host ([0.2](/mcp#02-the-two-endpoints)). ## 1.1 Get a key 1. Sign in at `https://dashboard.plungeai.com` → **One API → Keys** → create a key. Give it a name and pick an expiry (never, or 7 / 30 / 90 / 180 / 365 days). 2. Copy it now. It starts with `ozk_live_` and is shown exactly once. 3. Put it in your shell for the examples in this guide: ```bash export PLUNGE_API_KEY=ozk_… # paste your key; never commit it ``` Key details (tiers, expiry, revocation, fences): [Authentication & keys](/mcp/authentication). ## 1.2 Connect Claude Code (one command) ```bash claude mcp add --transport http plungeai https://mcp.plungeai.com/v1 \ --header "Authorization: Bearer $PLUNGE_API_KEY" --scope user ``` - `--scope user` makes the server available in every project. Without it, the default scope is `local` (this project only). Both are stored in `~/.claude.json`, outside your repo. - Never use `--scope project` with a literal key: that writes the key into `.mcp.json` inside the repo. Check it: `claude mcp list` (or `/mcp` inside a session) shows `plungeai` connected. Then ask Claude: > use plungeai_whoami to confirm my identity You get an identity card: user id, auth type and tier, key label, rate-limit window, server. Every other client is in [Connecting clients](/mcp/connecting-clients). ## 1.3 Or: a project `.mcp.json` To share the server definition with a team, commit an `.mcp.json` that reads the key from each developer's environment instead of holding it: ```json { "mcpServers": { "plungeai": { "type": "http", "url": "https://mcp.plungeai.com/v1", "headers": { "Authorization": "Bearer ${PLUNGE_API_KEY}" } } } } ``` Each developer exports `PLUNGE_API_KEY` before starting Claude Code. The same `mcpServers` shape works in other JSON-configured clients, with small per-client differences ([Connecting clients](/mcp/connecting-clients)). If you write a literal key into a file instead (`"Bearer ozk_YOUR_KEY"`), keep that file out of git. ## 1.4 Raw HTTP: `initialize` → `tools/list` → `tools/call` No client needed. The server speaks JSON-RPC 2.0 over Streamable HTTP, and it is stateless: send each call on its own, no session id. `initialize` is what a client sends first; you can skip it with `curl` and go straight to `tools/list`. **Step 1 — `initialize`.** The answer is plain JSON: the negotiated protocol version, the capabilities (`tools`, `resources`, `prompts`, `logging`), `serverInfo` (`plungeai.com`) and the server's usage instructions for AI clients. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' ``` **Step 2 — `tools/list`.** Plain JSON again: every tool with its `description`, its `inputSchema` (JSON Schema) and, for some, an `outputSchema` and a `readOnlyHint`. The full answer is long; this prints only the names: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \ | python3 -c 'import json,sys; print("\n".join(t["name"] for t in json.load(sys.stdin)["result"]["tools"]))' ``` You see 22 names, `plungeai_execute_workflow` first and `plungeai_connections` last. A key with a tool fence sees fewer ([2.5](/mcp/authentication#25-key-fences)). **Step 3 — `tools/call plungeai_whoami`.** Read-only and free (it still counts as one call toward your rate limit): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I"}}}' ``` A `tools/call` always answers as a server-sent-event stream, even a quick one: ``` event: message data: {"result":{"content":[{"type":"text","text":"# Who am I\n\n- **User:** `…`\n- **Auth:** api-key · tier **pro**\n- **Key:** my-laptop (`…`)\n- **Rate window:** 1/min used · 1/day used\n- **Server:** plungeai.com (mcp.plungeai.com)","annotations":{…}}],"isError":false},"jsonrpc":"2.0","id":3} ``` Longer calls send `notifications/message` (or `notifications/progress`) events first, then the result. The `data:` line of the last event is the JSON-RPC response. ### Reading a `tools/call` answer Pipe any `tools/call` through this to print just the rendered text of the final result: ```bash … | sed -n 's/^data: //p' | tail -1 \ | python3 -c 'import json,sys; r=json.load(sys.stdin)["result"]; print(r["content"][0]["text"])' ``` Notes: - `user_request` is optional on every tool, but pass it: it is the user's request in their own words, and the platform uses it for routing and support diagnostics. Clients are told to send it on every call. - If you leave out the `Accept` header, the server fills it in for you. MCP clients always send it. - A `401` here means the key did not reach the server or is not valid — [2.3](/mcp/authentication#23-what-an-unauthenticated-call-returns). ## 1.5 Your first real run **Find an agent.** Discovery is free. `search` is a hybrid semantic and keyword query, so describe the capability in plain words: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"plungeai_list_agents","arguments":{"user_request":"what agents can search the web?","search":"web search","limit":3}}}' ``` Each match is a registry card: id, category, what it is good at, and "Not for → use X instead" redirects. Take agent ids from a search like this, never from memory — an id that is not active is refused at execution ([Discovery](/mcp/discovery)). **Run one.** This calls `one-agent`, the platform's agent runtime, once. It runs a model, so it costs credits (no `# verify` marker): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"plungeai_execute_agent","arguments":{"user_request":"what is a CDN, in one sentence","agent":"one-agent","prompt":"In one sentence: what is a content delivery network?","max_tokens":80}}}' ``` While it runs, progress events arrive first (lines such as `⚙️ Agents working — 0/1 agents complete` or `✍️ Writing (212 chars) — …`). The final result's text is the answer, then a footer: ``` --- execution: `3f0c…` — use this id for plungeai_get_result / plungeai_followup on this run. ``` Its `structuredContent` is the outcome object `{"status":"ok","summary":"Agent completed.","result":"…","execution_id":"…"}`. The run now appears in your execution history in Studio and the dashboard. **Read it again.** `plungeai_get_result` fetches any of your runs by that execution id (read-only): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"plungeai_get_result","arguments":{"user_request":"show me that result again","execution_id":"PASTE_EXECUTION_ID"}}}' ``` An id that is not one of yours comes back as `needs_input` naming the id — not a bare "not found" ([Runs, results & follow-ups](/mcp/runs-results)). In a client you would simply ask: *"Ask one-agent in one sentence what a Cloudflare Durable Object is."*, then *"Show me the result of that last run."* ## 1.6 What to try next **The `/plungeai` prompt.** In Claude Code, type `/plungeai` and pick `/plungeai:plungeai (MCP)` from the menu. Run it bare, then type your request as the next message: Claude Code passes only the first word of any text typed after the command. You can also just talk normally — the prompt is a convenience ([Prompts & resources](/mcp/prompts-resources)). **Things to ask**, as normal sentences: | Try asking | What happens | Chapter | |---|---|---| | "What agents do you have for financial analysis and SEC filings?" | semantic registry search | [Discovery](/mcp/discovery) | | "Give me a five-day weather forecast for Sun Valley, including air quality." | the client reads `weather-agent`'s contract and runs it with typed fields | [Tools](/mcp/tools) | | "Ask your harness agent a quick question: what's notable about Sun Valley, Idaho?" | a bounded one-shot mission | [Agents & missions](/mcp/agents-missions) | | "Show me all the workflows in my Finance folder." · "Run the Acquisition Scanner with Adobe as the input." | lists and runs your workflows | [Workflows](/mcp/workflows) | | "Build me a workflow that scans a company for acquisition news and writes a briefing." | generates and saves a CNL workflow from real registry agents | [Workflows](/mcp/workflows) | | "Is that still running?" · "Show me my last 10 executions." | status and run history | [Runs, results & follow-ups](/mcp/runs-results) | | "Remember that our fiscal year ends in June." | writes durable memory | [Chat, memory & skills](/mcp/chat-memory-skills) | | "Run the S&P 500 weekly performance analysis every Monday at 7am." | creates a cron schedule | [Scheduling](/mcp/scheduling) | | "Who am I, what tier am I on, and how much of my rate limit have I used?" | `plungeai_whoami` | [Authentication & keys](/mcp/authentication) | **When a run pauses.** Some runs stop before spending money or doing something irreversible and come back headed `⏸ NEEDS APPROVAL`. Nothing has run yet. If the answer carries `⏸ AWAITING USER APPROVAL: …`, an agent staged the action and paused: your client relays it, you answer "yes" or anything else, and the client continues with `plungeai_continue`. If it does not, the trust fence refused an outward operation (a send, a payment) outright: there is nothing to continue over MCP, and the operation runs only from Ocean Studio, where you approve it ([6.6](/mcp/tools#66-the-trust-fence--needs-approval), [Runs, results & follow-ups](/mcp/runs-results)). **Long jobs.** Ask for them to run "in the background": you get an execution id at once and ask for the status or result later ([Workflows](/mcp/workflows)). # Authentication & keys Source: https://www.plungeai.com/docs/mcp/authentication Source: `orchestration/mcp-gateway/auth-middleware.ts`, `index.ts`, `rate-limiter.ts`, `core/core-api-keys/rate-limit-config.ts`, `extras.ts` (`plungeaiWhoami`), `core/core-api-keys/index.ts`, `apps/ocean-dashboard/src/worker/routes/platform-keys.routes.ts`, `orchestration/api-gateway/lib/auth.ts`, `orchestration/api-gateway/routes/mcp.ts`. Unauthenticated `401` responses live-verified 2026-09-24. Every MCP call needs an `ozk_` key. The key **is** your identity: it resolves to one user account, and every tool reads and writes only what that account owns. There is no OAuth sign-in on the MCP server today; bearer keys are the only way in. This chapter uses `https://mcp.plungeai.com/v1`; `https://api.plungeai.com/v1/mcp` accepts the same key, with the differences noted in each section. ## 2.1 The `ozk_` key | Property | Value | |---|---| | Format | `ozk_live_` + 48 hex characters | | Where | Dashboard → **One API → Keys** (`https://dashboard.plungeai.com`), self-service while signed in | | Name | required, 1–64 characters | | Expiry | never, or 7 / 30 / 90 / 180 / 365 days, chosen when you create it | | Shown | exactly once, at creation. It is stored only as a SHA-256 hash; nobody can show it to you again | | Per account | up to 25 active keys; the 26th is refused until you revoke one | | Tier | copied from your account's tier when the key is created (`free`, `pro`, `enterprise`; an unknown account tier gives `free`) | | Works on | the MCP server and every One API plane, models included (`https://api.plungeai.com/docs#2-authentication--keys`) | The Keys list shows each key's name, redacted prefix and last four characters, created and last-used dates, expiry (highlighted when it is less than a week away) and status (`active`, `revoked`, `expired`), with a revoke action on active keys. Because the tier is copied at creation, a key keeps the tier it was created with. After your account changes tier, create a new key to get the new limits; `plungeai_whoami` shows the tier of the key you are using ([2.7](#27-checking-yourself--plungeai_whoami)). A **shared team key** is simply a key created under someone else's account: whoever uses it acts as that account. Ask the account owner for one; you cannot create a key for another account yourself. ## 2.2 Sending the key | Header | Example | |---|---| | `Authorization: Bearer <key>` | `-H "Authorization: Bearer $PLUNGE_API_KEY"` | | `X-API-Key: <key>` | `-H "X-API-Key: $PLUNGE_API_KEY"` | Both are **supported**. The server tries `X-API-Key` first, then `Authorization: Bearer`. A bearer value that does not start with `ozk_` is **rejected** (`401`): OAuth tokens and `sk-ocean-` model keys (legacy) are not credentials here. `X-API-Key` exists for integrations whose "API key" field does not send a bearer header: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "X-API-Key: $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` On `https://api.plungeai.com/v1/mcp` both headers work too, and the value must start with `ozk_`. ## 2.3 What an unauthenticated call returns Any request to `/v1` without a valid key gets `401` with a JSON-RPC error body and a `WWW-Authenticate` challenge. No credentials at all gives a bare `Bearer` challenge (live-verified 2026-09-24): ```bash curl -s -o /dev/null -w '%{http_code} %header{www-authenticate}\n' \ -X POST https://mcp.plungeai.com/v1 \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` A credential that was sent but is not valid adds `error="invalid_token"` (live-verified 2026-09-24, with a made-up key): ```bash curl -s -o /dev/null -w '%{http_code} %header{www-authenticate}\n' \ -X POST https://mcp.plungeai.com/v1 \ -H 'Authorization: Bearer ozk_live_not_a_real_key' \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` The body is the same in both cases: ```bash curl -s -X POST https://mcp.plungeai.com/v1 \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` ```json {"jsonrpc":"2.0","error":{"code":-32001,"message":"No valid authentication provided. Use an X-API-Key header or \"Authorization: Bearer <api key>\"."},"id":null} ``` What each failure looks like: | Cause | Status | `WWW-Authenticate` | `error.message` | |---|---|---|---| | no key | `401` | `Bearer` | `No valid authentication provided. …` | | unknown, mistyped, revoked or expired key, or a non-`ozk_` bearer | `401` | `Bearer error="invalid_token"` | `No valid authentication provided. …` — the exact reason is logged server-side, not returned | | valid key used from an address outside its `allowed_ips` fence | `401` | `Bearer error="invalid_token"` | `API key not permitted from <your IP>` | | more than 20 failed attempts in a minute (or 500 in a day) from one IP | `429` + `Retry-After` | — | `Too many failed authentication attempts` | The same request on `https://api.plungeai.com/v1/mcp` answers in the One API envelope instead (live-verified 2026-09-24): ```bash curl -s -X POST https://api.plungeai.com/v1/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` There: no key or a non-`ozk_` value → `401 Bearer ozk_ key required`; an `ozk_` key that is invalid, revoked, expired or outside its IP fence → `401 Invalid or revoked API key`; too many failures → `429 rate_limited` (`too many failed authentications`). Every error carries `X-Error-Code` and `x-request-id` headers. The server does not publish OAuth discovery metadata (`/.well-known/oauth-protected-resource`). Clients that can only connect through OAuth need a bridge — see [Connecting clients](/mcp/connecting-clients). ## 2.4 Expiry, revocation and rotation - **Expiry** is checked on every request. From the moment a key expires it answers `401`. - **Revoke** a key from the same Keys list. The record stays (so the list and audit trail keep it) and is marked revoked. Each server instance caches a successfully checked key for up to 60 seconds, and the key store's own edge cache can add up to another 60 seconds, so a revoked key can keep working for up to about two minutes (about 75 seconds measured), then answers `401`. If the key leaked, revoke it first and allow for that window before you treat it as dead. - **Rotate** without downtime: create the new key, update every client config, confirm it with `plungeai_whoami` ([2.7](#27-checking-yourself--plungeai_whoami)), then revoke the old key. - **Leaked or lost?** Create a replacement and revoke the old one yourself. A lost key cannot be recovered: only its hash is stored. ## 2.5 Key fences A key can carry two optional fences. Keys you create yourself in the Dashboard have **no fences** (every tool, from any address). Fenced keys are issued by PlungeAI for you — ask the account owner who gave you the key, or [Requests](https://www.plungeai.com/request/new?kind=question). | Fence | Effect on `https://mcp.plungeai.com/v1` | |---|---| | `allowed_ips` | The key authenticates only from the listed addresses: exact IPv4 or IPv6 addresses, or IPv4 CIDR blocks (IPv6 ranges are not supported — list exact IPv6 addresses). From anywhere else: `401 API key not permitted from <ip>`, counted as a failed attempt. | | `allowed_tools` | Only the listed tools are registered for this key: the others do not appear in `tools/list`, and a `tools/call` to one returns JSON-RPC error `-32602` `Tool not permitted for this key: <name>`. | If a tool this guide documents is missing from your `tools/list`, or a call is refused this way, the key is fenced: that is policy, not a fault. ## 2.6 Tiers and rate limits Default limits per tier, from `tier-limits.ts` (the operator can change them, see the last point below): | Tier | Tool-call requests per minute | Tool-call requests per day | |---|---|---| | free | 30 | 1,000 | | pro | 100 | 10,000 | | enterprise | 300 | 100,000 | How they are counted: - **Per account, not per key.** The counter is keyed by your user id, so every key on your account shares it. The tier and any per-key override of the key you are using decide the cap. - **Only `tools/call` counts** on `https://mcp.plungeai.com/v1`: each HTTP request that carries one counts once (a JSON-RPC batch is one request). `initialize`, `tools/list`, `resources/*` and `prompts/*` are free. `plungeai_whoami` is a tool call and counts. - On `https://api.plungeai.com/v1/mcp` the rule is the same: the One API does not count `POST /v1/mcp`, and this server counts each request that carries a `tools/call`, with your key's per-key override. - **Fixed windows.** A minute window opens with your first counted call and resets 60 seconds later; a day window lasts 24 hours from its first call, not until midnight. - A per-key `rate_limit` override, when PlungeAI sets one on a key, replaces the per-minute cap. - **Set by the operator.** The numbers above are the defaults. PlungeAI sets the per-tier numbers, or switches the limits off, in Operator › One API › Keys › Rate limits (`GET`/`PUT /admin/rate-limits` for internal callers). While they are off, no call is counted and no tool call or One API `POST` returns `429`; the one limit left is the failed-authentication meter (20 invalid-key attempts a minute per client IP), which is always on. A change reaches the gateways within a minute. A per-key `rate_limit` still applies while limits are on. When you are over the limit, the call is refused before anything runs: ``` HTTP/2 429 retry-after: 42 {"jsonrpc":"2.0","error":{"code":-32000,"message":"Rate limit exceeded: Rate limit: 100 requests/minute","data":{"retryAfter":42}},"id":3} ``` `Retry-After` (and `error.data.retryAfter`) is the number of seconds until the window resets. Wait that long, then continue; do not retry in a tight loop. On the One API host the body is `{"error":{"code":"rate_limited","message":"rate limit exceeded for tier pro","retry_after":42}}`. The full error catalogue is in [Errors, feedback & limits](/mcp/errors-limits). ## 2.7 Checking yourself — `plungeai_whoami` `plungeai_whoami` shows who the server thinks you are and how much of your window you have used. It is read-only. > Try asking: *"Who am I, what tier am I on, and how much of my rate limit have I used?"* — or, right after connecting a client, *"use plungeai_whoami to confirm my identity"*. | Parameter | Type | Required | Default | Notes | |---|---|---|---|---| | `user_request` | string (≤ 4,000 chars) | no | — | the user's own words | | `format` | `markdown` · `json` | no | `markdown` | `json` returns the fields below as a JSON document plus `structuredContent` | ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"how much of my rate limit have I used?"}}}' ``` The Markdown answer: ``` # Who am I - **User:** `<your user id>` - **Auth:** api-key · tier **pro** - **Key:** my-laptop (`<key id>`) - **Rate window:** 3/min used · 41/day used - **Server:** plungeai.com (mcp.plungeai.com) ``` With `format: "json"`: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I, as JSON","format":"json"}}}' ``` ```json { "user": "<your user id>", "auth_type": "api-key", "tier": "pro", "key_label": "my-laptop", "key_id": "<key id>", "rate_window": { "minute_used": 3, "day_used": 41 }, "server": "plungeai.com (mcp.plungeai.com)" } ``` What to know: - The counts include the `plungeai_whoami` call itself. Compare them with your tier's caps in [2.6](#26-tiers-and-rate-limits); the card shows usage, not the caps. - The rate-window line is best-effort. If the counter cannot be read, the line is absent (`rate_window: null`); that alone is not an error. - There is no permissions field. A tool fence shows up as fewer tools in `tools/list`, not on this card. - Through `https://api.plungeai.com/v1/mcp` the card is the same: the One API forwards your key and the gateway reads its record. - Without an identity the tool answers `Authentication required.` with `isError: true` — in practice you get the `401` from [2.3](#23-what-an-unauthenticated-call-returns) first. ## 2.8 Identity and scoping - Everything is scoped to the account behind the key: executions, workflows, schedules, conversations, memory. You see what that account owns, the same view it has in Studio — never the whole system. - Id-based lookups check ownership before reading anything. Someone else's id, or a mistyped one, answers "not found", never "forbidden" (`plungeai_get_result` answers with a `needs_input` outcome naming the id). An id that looks right but is "not found" usually belongs to another account or is mistyped. - What you create through MCP carries your user id, so it shows up in Studio, the dashboard and the other PlungeAI apps under your account. - Every authenticated request is written to your activity trail with its arguments and answer. You can read your own day at `https://mcp.plungeai.com/log?date=YYYY-MM-DD` with the same key ([Errors, feedback & limits](/mcp/errors-limits)). This is also why `user_request` matters: it is where your own sentence survives into support diagnostics. ## 2.9 Keeping the key safe - Treat it like a password. Anyone holding it acts as your account. - Keep the literal key in user-level config (`~/.claude.json`, `~/.cursor/mcp.json`, the client's secret store) or an environment variable. A config file inside a repo should only reference a variable ([Connecting clients](/mcp/connecting-clients) shows how per client). - Before the first commit in a repo you just configured, both of these must print nothing: ```bash git grep -n "ozk_" # tracked files grep -rn "ozk_" . --exclude-dir=.git --exclude-dir=node_modules # untracked files too ``` - Never put the key in browser code or a client bundle. Apps call PlungeAI from a server with the key in a server-side secret. - If a key may have leaked, rotate it now ([2.4](#24-expiry-revocation-and-rotation)). # Connecting clients Source: https://www.plungeai.com/docs/mcp/connecting-clients Source: `orchestration/mcp-gateway/integrations.ts`, `install-routes.ts`, `platform/src/worker/install.ts` (the install page on www), `llms-routes.ts`, `docs-routes.ts`, `skill-dist.generated.ts`, `index.ts`; `apps/ocean-skills/skills/plungeai-in-*/SKILL.md` (per-client facts, checked against vendor docs by that skill set), `apps/ocean-skills/skills/plungeai-mcp-setup/SKILL.md`, `apps/ocean-skills/.mcp.json` (the plugin's MCP server), `apps/ocean-skills/scripts/mirror-public.sh`; live `GET /install`, `/install.json`, `/install/:tool`, `/connect/<tool>/SKILL.md` and `https://skills.plungeai.com/index.json` (live-verified 2026-09-24). Every client needs the same two things: the URL `https://mcp.plungeai.com/v1` and the header `Authorization: Bearer ozk_YOUR_KEY`. What differs is where each client keeps its config and what it calls the fields. `https://api.plungeai.com/v1/mcp` is the same server and works in any of these configs; this chapter uses the canonical URL. | Client | How it connects | Config lives in | On the install page | Setup skill | |---|---|---|---|---| | Claude Code | native HTTP, `claude mcp add` | `~/.claude.json`, or a project `.mcp.json` | command | `/connect/claude-code/SKILL.md` | | Claude Desktop | `mcp-remote` stdio bridge | `claude_desktop_config.json` | config snippet | `/connect/claude-ai/SKILL.md` | | claude.ai (web) | skill upload only — no live tools yet | Customize → Skills | steps | `/connect/claude-ai/SKILL.md` | | Cursor | native HTTP | `~/.cursor/mcp.json` | **Add to Cursor** button | `/connect/cursor/SKILL.md` | | VS Code (Copilot) | native HTTP | `.vscode/mcp.json` or the user `mcp.json` | **Add to VS Code** button | `/connect/vscode/SKILL.md` | | Codex CLI | native streamable HTTP | `~/.codex/config.toml` | command (ChatGPT / Codex card) | `/connect/codex/SKILL.md` | | Gemini CLI | native HTTP (`httpUrl`) | `~/.gemini/settings.json` | — | `/connect/gemini-cli/SKILL.md` | | OpenCode | native remote (`type: remote`) | `~/.config/opencode/opencode.json` | — | `/connect/opencode/SKILL.md` | | Windsurf | native HTTP (`serverUrl`) | `~/.codeium/windsurf/mcp_config.json` | config snippet | `/connect/windsurf/SKILL.md` | | Replit | Integrations → MCP Servers | your Replit account | **Add to Replit** button | `/connect/replit/SKILL.md` | | Lovable | Connectors → MCP server | your Lovable account | steps | `/connect/lovable/SKILL.md` | | Bolt.new | Connectors → Custom MCP server | your Bolt account | steps | `/connect/bolt/SKILL.md` | | v0 | + → MCPs | your v0 account | steps | `/connect/v0/SKILL.md` | Setup skill paths are on `https://mcp.plungeai.com` (for example `https://mcp.plungeai.com/connect/cursor/SKILL.md`); the same skills are on `https://skills.plungeai.com` as `plungeai-in-<tool>` ([3.16](#316-the-skills-catalog)). ## 3.1 Before you start **Transport.** The server speaks Streamable HTTP (clients call it "http", "remote" or "streamable-http"). A client that can only launch local stdio servers connects through the `mcp-remote` bridge ([3.14](#314-any-other-mcp-client)). A client that can only speak the old SSE transport cannot connect: the server answers `GET` with `405` at once. **Where the key goes.** Put the literal key only in a user-level config file or the client's secret store. A config file inside a repository should reference an environment variable instead; each section below shows the client's syntax for that. This guide names the variable `PLUNGE_API_KEY`; the setup skills and the install page use `PLUNGEAI_API_KEY`. Any name works as long as your config and your shell agree. Before committing, run the `git grep` check in [2.9](/mcp/authentication#29-keeping-the-key-safe). **Check the key outside the client first.** If this returns your identity card, the key is good and any remaining problem is in the client config: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"verify my plungeai connection"}}}' ``` **Verify in the client.** The same three checks work everywhere: 1. The client's MCP panel or command shows `plungeai` connected, with tools listed. 2. Ask: *"use plungeai_whoami to confirm my identity"* → an identity card (user id, tier, key label, rate window, server). 3. Ask: *"use plungeai_list_agents to search 'web search'"* → live registry results. This proves the catalog is reachable, not just the key. **Reload after every edit.** Clients read the tool list when they connect. After changing the config — or after a platform update adds a tool — restart the client or reload its MCP servers before debugging anything else. **Approvals are server-side.** A client's auto-approve or auto-run setting only skips the client's own confirmation prompt. Outward, irreversible operations (payments, sends) still stop with a `needs_approval` outcome. An agent's staged action continues only after your explicit answer; a trust-fence refusal cannot be continued over MCP at all and runs only from Ocean Studio ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)). **Tool caps.** Some clients limit how many tools they send to the model across all connected servers. PlungeAI adds 22 (fewer with a fenced key). If `plungeai_*` tools are missing, turn off tools or servers you do not use. ## 3.2 Claude Code Terminal one-liner (the key expands from your shell and is stored in `~/.claude.json`): ```bash claude mcp add --transport http plungeai https://mcp.plungeai.com/v1 \ --header "Authorization: Bearer $PLUNGE_API_KEY" --scope user ``` - `--scope user`: every project. Default `local`: this project only. Both live in `~/.claude.json`. - Never `--scope project` with a literal key — it writes the key into the repo's `.mcp.json`. Team-shared project file (safe to commit — it holds no key; each developer exports `PLUNGE_API_KEY` before starting Claude Code): ```json { "mcpServers": { "plungeai": { "type": "http", "url": "https://mcp.plungeai.com/v1", "headers": { "Authorization": "Bearer ${PLUNGE_API_KEY}" } } } } ``` **Verify:** `claude mcp list` (or `/mcp` in a session) shows `plungeai` connected; then the three checks in [3.1](#31-before-you-start). **Quirks:** - `401` (`No valid authentication provided…`): the header never arrived or the key is wrong. Re-check the header field in `~/.claude.json` / `.mcp.json` rather than the key value, and look for whitespace pasted into the key. - Connected but no `plungeai_*` tools: restart the session after any config edit. - Long CNL workflows: ask for them to run in the background (`mode: "async"` + `plungeai_get_workflow_status`) rather than one long synchronous call. - Type `/plungeai` and pick `/plungeai:plungeai (MCP)` to start from the server's prompt; run it bare and give your request in the next message ([Prompts & resources](/mcp/prompts-resources)). - **Skills for Claude Code** (conventions, CNL authoring): working today, download a zip from `https://skills.plungeai.com` (or `https://mcp.plungeai.com/skill.zip`, the workflow-authoring skill) and unzip it into `.claude/skills/` or `~/.claude/skills/`. When the public repo is published: `claude plugin marketplace add PlungeAI/plungeai-agent-skills`, then `claude plugin install plungeai@plungeai` ([3.16](#316-the-skills-catalog)). The plugin also registers its own `plungeai` MCP server (header `Bearer ${PLUNGEAI_API_KEY}`), so install it *instead of* `claude mcp add`, not in addition, and export `PLUNGEAI_API_KEY`. If you already added the server by hand, keep only one of the two. ## 3.3 Claude.ai and Claude Desktop Two surfaces, two capabilities: | Surface | What it gets | |---|---| | **Claude Desktop** | the live `plungeai_*` tools, through the `mcp-remote` bridge | | **claude.ai (web)** | a PlungeAI **skill** only: Claude learns PlungeAI's conventions and writes paste-ready CNL workflows for Studio, but has no live tool calls | Why a bridge: the custom-connector UI in claude.ai and Desktop (Settings → Connectors → Add custom connector) authenticates only through OAuth and has no field for a bearer header, and the PlungeAI MCP server has no OAuth yet. **Claude Desktop.** Needs Node.js (the bridge runs through `npx`). Open Settings → Developer → **Edit Config**; the file is `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Merge: ```json { "mcpServers": { "plungeai": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.plungeai.com/v1", "--header", "Authorization: Bearer ozk_YOUR_KEY"] } } } ``` Quit Claude Desktop completely and start it again; the file is read only at start. **claude.ai web.** First turn on **Settings → Capabilities → Code execution and file creation** (required for any custom skill). Then **Customize → Skills → + → Upload a skill** and upload `https://mcp.plungeai.com/skill.zip` (the workflow-authoring skill; start with this one), or a single-skill zip from `https://skills.plungeai.com` (`https://skills.plungeai.com/<name>.zip`), one skill per upload. Skill names may not contain the reserved word "claude", so `plungeai-in-claude-ai` and `plungeai-in-claude-code` cannot be uploaded to claude.ai. The all-in-one `plungeai-agent-skills.zip` holds all 32 skills; it is for unzipping into a skills folder, not for upload. **Verify:** Desktop — the three checks in [3.1](#31-before-you-start). claude.ai web has no live call to check; ask Claude to draft a PlungeAI CNL workflow to confirm the skill loaded. **Quirks:** - **Windows:** Desktop does not escape spaces inside `args`. If the bridge answers `401` only on Windows, use `"--header", "Authorization:${AUTH_HEADER}"` and add `"env": { "AUTH_HEADER": "Bearer ozk_YOUR_KEY" }` to the same entry. - Logs: `~/Library/Logs/Claude/mcp*.log` (macOS), `%APPDATA%\Claude\logs` (Windows). Read them before editing the config again. - The bridge is an `npx` process; a cold start takes a few seconds. If the tools never appear, wait, then check the logs. - Claude Desktop caps a single tool call at about 4 minutes. For anything longer, ask for it to run "in the background": you get an execution id at once and ask for the status or result later. - `401` through the bridge with a key that looks right: whitespace, or a missing `ozk_` prefix — paste the key again. ## 3.4 Cursor **One click.** Open `https://www.plungeai.com/install`, type your `ozk_` key into the box at the top, then click **Add to Cursor** and confirm Cursor's install dialog. The link is rewritten in your browser to include the header. Clicked without a key, the server installs with no header (it shows red, `401`) and you add the `headers` block by hand. **By hand**, in `~/.cursor/mcp.json` (user-level — the right place for a literal key): ```json { "mcpServers": { "plungeai": { "url": "https://mcp.plungeai.com/v1", "headers": { "Authorization": "Bearer ozk_YOUR_KEY" } } } } ``` A project `.cursor/mcp.json` gets committed; there, reference a variable: `"Authorization": "Bearer ${env:PLUNGE_API_KEY}"` (Cursor interpolates `${env:…}` in `command`, `args`, `env`, `url` and `headers`). **Verify:** Settings → MCP → `plungeai` has a green dot and lists tools; then the checks in [3.1](#31-before-you-start) in Agent chat. **Quirks:** - MCP tools run in **Agent** and **Plan** modes. **Ask** mode is read-only and does not call them. - Tool calls ask for approval per call by default. - Red status or "No tools found" right after install: check auth first — the `headers` object must sit *inside* the `plungeai` entry, and the key must be intact. - After editing `mcp.json`, toggle the server off and on in Settings → MCP (or restart Cursor). - The `plungeai-in-cursor` skill notes a reported cap of roughly 40 MCP tools across all servers (not in Cursor's current docs). If `plungeai_*` tools are missing, turn off unused servers or tools and reopen the chat. - Keep sessions discovery-first with a line in `.cursor/rules/plungeai.mdc`: "PlungeAI: agent ids come from a live `plungeai_list_agents` search — never from memory." Rules files are committed; never put the key there. ## 3.5 VS Code (GitHub Copilot) **From a terminal** (simplest): ```bash code --add-mcp '{"name":"plungeai","type":"http","url":"https://mcp.plungeai.com/v1","headers":{"Authorization":"Bearer ozk_YOUR_KEY"}}' ``` In PowerShell or cmd, double-quote the JSON and escape the inner quotes with `\"`. **One click:** type your key into the box on `https://www.plungeai.com/install`, click **Add to VS Code**, confirm the MCP dialog. **Committed-safe workspace file** `.vscode/mcp.json`: the `inputs` block makes VS Code prompt for the key once and store it encrypted, so the file never holds the key: ```json { "inputs": [ { "id": "plungeai-key", "type": "promptString", "password": true, "description": "PlungeAI ozk_ key" } ], "servers": { "plungeai": { "type": "http", "url": "https://mcp.plungeai.com/v1", "headers": { "Authorization": "Bearer ${input:plungeai-key}" } } } } ``` Machine-wide instead: **MCP: Open User Configuration**, or **MCP: Add Server** with the user scope — same fields, outside any repo. **Verify:** **MCP: List Servers** shows `plungeai` running and the chat Tools picker lists `plungeai_*`; then the checks in [3.1](#31-before-you-start) in Copilot agent chat. **Quirks:** - MCP tools run in **agent mode** (Chat view → mode picker → Agent). - At most **128 tools per chat request** across all sources. Over the limit, deselect servers or tools in the Tools picker, or let `github.copilot.chat.virtualTools.threshold` group tools beyond the threshold. - Mistyped key: **MCP: List Servers** → `plungeai` → **Show Output** shows the `401`. Restart the server from the same menu; if it does not ask for the stored input again, remove and re-add the server. - Locked-down setups: `chat.mcp.access` decides which MCP servers may run, and organisation policies (`ChatAllowedMcpServers`, `ChatDeniedMcpServers`, `ChatAllowManagedMcpServersOnly`) can allow- or deny-list servers. A missing MCP section in settings usually means one of these. - VS Code asks you to confirm you trust a server before its first start, except when it is started directly from `mcp.json`. ## 3.6 Codex CLI (and ChatGPT) Codex CLI supports remote streamable-HTTP servers natively. In `~/.codex/config.toml`: ```toml [mcp_servers.plungeai] url = "https://mcp.plungeai.com/v1" bearer_token_env_var = "PLUNGE_API_KEY" ``` Export `PLUNGE_API_KEY` in your shell profile; the file holds no key. Or add it from the terminal: ```bash codex mcp add plungeai --url https://mcp.plungeai.com/v1 --bearer-token-env-var PLUNGE_API_KEY codex mcp list ``` A static header also works, in the user-level file only: `http_headers = { Authorization = "Bearer ozk_YOUR_KEY" }`. Fallback for a build without native `url` support — the `mcp-remote` bridge: ```toml [mcp_servers.plungeai] command = "npx" args = ["-y", "mcp-remote", "https://mcp.plungeai.com/v1", "--header", "Authorization: Bearer ozk_YOUR_KEY"] startup_timeout_sec = 30 ``` `startup_timeout_sec` defaults to 10 seconds; an `npx` cold start can exceed it, and then the tools never appear with no useful error. If the bridge answers `401`, Codex is splitting the spaces inside `args` — use the native form above. **Verify:** `codex mcp list` shows `plungeai` (or `/mcp` in the TUI shows it connected); then the checks in [3.1](#31-before-you-start). **Quirks:** - `config.toml` is TOML: no braces around the table, no trailing commas, and the header is exactly `[mcp_servers.plungeai]` (underscore). - `tool_timeout_sec` defaults to 60 seconds. Run long workflows with `mode: "async"` and poll `plungeai_get_workflow_status`. - `codex mcp get plungeai` and `codex mcp remove plungeai` manage the entry; `enabled = false` turns it off without deleting it. - **ChatGPT (web):** not supported yet. ChatGPT connectors need OAuth, and PlungeAI OAuth is in progress. ChatGPT users connect through Codex CLI for now. ## 3.7 Gemini CLI Merge into `~/.gemini/settings.json` (user-level): ```json { "mcpServers": { "plungeai": { "httpUrl": "https://mcp.plungeai.com/v1", "headers": { "Authorization": "Bearer ozk_YOUR_KEY" } } } } ``` **The key is `httpUrl`, not `url`.** Gemini CLI uses `url` for SSE servers; this server answers an SSE-style `GET` with `405`, so a `url` entry never connects. This is the most common mistake with this client. If your build has no `httpUrl`, bridge instead: ```json { "mcpServers": { "plungeai": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.plungeai.com/v1", "--header", "Authorization: Bearer ozk_YOUR_KEY"] } } } ``` A project `.gemini/settings.json` gets committed — keep the literal key in the user-level file. **Verify:** `/mcp` in the CLI lists `plungeai` with its tools; then the checks in [3.1](#31-before-you-start). **Quirks:** - `/mcp reload` reconnects the servers and re-reads their tools after a settings edit. (`/mcp refresh` exists on some builds but has had reload bugs upstream.) If neither picks up the change, restart the CLI. - Tool calls ask for confirmation by default; `"trust": true` on the server entry skips that prompt. Platform approvals still pause. - A setup that used to work and now shows no tools usually means the key was revoked or expired — run the `curl` check in [3.1](#31-before-you-start) before touching the config. - A `GEMINI.md` line such as "PlungeAI agent ids come from a live `plungeai_list_agents` search — never from memory" keeps sessions discovery-first. Context files are committed; never put the key there. ## 3.8 OpenCode Merge into `~/.config/opencode/opencode.json` (user-level) or the project's `opencode.json`: ```json { "mcp": { "plungeai": { "type": "remote", "url": "https://mcp.plungeai.com/v1", "headers": { "Authorization": "Bearer ozk_YOUR_KEY" } } } } ``` The top-level key is `mcp` (not `mcpServers`), and a remote server needs `"type": "remote"`. In a committed project file, use variable substitution: `"Authorization": "Bearer {env:PLUNGE_API_KEY}"` (`{file:path}` also exists, for a key file). **Verify:** the checks in [3.1](#31-before-you-start). **Quirks:** - The project config merges over the user config. If tools behave oddly, check whether a committed `opencode.json` (or `.jsonc`) redefines `mcp.plungeai`, for example with `"enabled": false`. - `{env:…}` is resolved at startup: export the variable before launching OpenCode, or the header goes out empty and you get `401`. - MCP servers connect at startup; restart the session after any config edit. - OpenCode reads `AGENTS.md` — the place for a discovery-first reminder. It is committed; never put the key in it. ## 3.9 Windsurf Windsurf has no install-page button; it is configured by file. Merge into `~/.codeium/windsurf/mcp_config.json` (user-level), then refresh the MCP servers in Cascade's MCP panel or restart Windsurf: ```json { "mcpServers": { "plungeai": { "serverUrl": "https://mcp.plungeai.com/v1", "headers": { "Authorization": "Bearer ozk_YOUR_KEY" } } } } ``` `serverUrl` is the documented key for remote servers (Windsurf also accepts `url`, but its examples use `serverUrl`). Older stdio-only builds use the `mcp-remote` bridge, with the same `command`/`args` block as in [3.7](#37-gemini-cli). **Verify:** Cascade's MCP panel shows `plungeai` with tools after a refresh; then the checks in [3.1](#31-before-you-start) in Cascade. **Quirks:** - Config edits are not hot-reloaded: refresh the MCP panel (or restart) after every change. - Cascade has a limit of roughly 100 tools across all servers. If `plungeai_*` tools are missing, turn off unused servers and refresh. - Discovery-first rules go in `.windsurf/rules/*.md` (the older `.windsurfrules` is still read) or `~/.codeium/windsurf/memories/global_rules.md`. Rules files are committed; never put the key there. ## 3.10 Replit Replit's MCP integration is for **Replit Agent while it builds**. Apps the Agent ships call the One API (`https://api.plungeai.com`) with their own key in Replit Secrets. **One click:** type your key into the box on `https://www.plungeai.com/install`, then click **Add to Replit**; the integration is imported with its `Authorization` header. Clicked without a key, add the header afterwards under the integration's **Advanced settings**. **By hand:** Integrations → **MCP Servers** → **+ Add MCP server** → Display name `PlungeAI`, Server URL `https://mcp.plungeai.com/v1`, then **Advanced settings** → custom header `Authorization` = `Bearer ozk_YOUR_KEY`. The integration is account-level: the Agent gets the `plungeai_*` tools in all your projects. **Verify:** ask the Agent *"use plungeai_whoami to confirm my identity"*, then *"use plungeai_list_agents to search 'web search'"*. **Quirks:** - Two key locations, never mixed: the integration's header (for the Agent) and **Replit Secrets** (for your Repl's own code). Never put the key in source files, `.replit` or `replit.nix`. - An app that works in the workspace but answers `401` after deploy: the deployment's secrets are missing the key. - A tool that requires confirmation prompts before it runs; platform approvals still pause regardless. - The usual split when you ask the Agent to "build an app that uses PlungeAI": it uses MCP to find and test the right agent, then writes app code that calls the One API with the key from Replit Secrets. ## 3.11 Lovable Two connectors, two purposes. **Chat connector (any plan).** Connectors → **+** → **MCP server** → Server name `PlungeAI`, Server URL `https://mcp.plungeai.com/v1`, Authentication → bearer token / API key → paste your `ozk_` key. Lovable's chat now has the `plungeai_*` tools while it builds. - Chat connectors are **personal**: each workspace member connects the server with their own key. - They are **build-time only** and never part of the published app. - Workspace admins can switch custom MCP servers off for everyone (Connectors → Admin settings → Chat connectors → "Custom MCP"). A member who cannot find "MCP server" in the picker is hitting that switch. **App connector (workspace admin).** So that apps built in Lovable call PlungeAI in production, an admin creates a custom connector for the One API. Connectors → **+** → **Custom connector**: | Field | Value | |---|---| | Display name | `PlungeAI` | | Category | Development | | Documentation URL | `https://www.plungeai.com/docs/one-api` | | Authentication method | Bearer token | | API base URL | `https://api.plungeai.com` | | Test request | `GET /v1/agents` | Members connect it to their apps with their own key; Lovable checks the key against the test request (green check = valid) and attaches it to every One API call the app makes, so generated code must never add its own `Authorization` header. The full admin pack, including the agent knowledge to attach to the connector, is at `https://mcp.plungeai.com/lovable/SETUP.md`. **Verify:** chat — the checks in [3.1](#31-before-you-start); app connector — the green check when an app is connected. **Quirks:** - If generated code contains an `Authorization` header, the connector knowledge was not attached or the connector step was skipped. - The app connector is admin-only. Non-admins use the chat connector for building and hand `SETUP.md` to their admin. ## 3.12 Bolt.new Bolt is a hosted builder: no config file, only the connector form. From the Bolt home chatbox: **+** → **Connectors** → **Manage connectors** → **Custom MCP server**: | Field | Value | |---|---| | Name | `PlungeAI` | | URL | `https://mcp.plungeai.com/v1` | | Transport type | `HTTP` | | Authentication | **API key** → paste your `ozk_` key (Bolt's "MCP OAuth" option does not work with PlungeAI yet) | Bolt does not document which header its API-key field sends. The server accepts both `X-API-Key: ozk_…` and `Authorization: Bearer ozk_…`, so the raw key works; confirm with the checks below. **Verify:** in the builder chat, the checks in [3.1](#31-before-you-start). **Quirks:** - **Auto-enable for all projects** turns the connector on in each new project; existing projects enable it from the same Connectors menu. - Adding a connector turns all its tools on, and the tool list is global — you cannot choose tools per project. - Tools missing mid-session: in Manage connectors, check the connector is enabled for this project and its tools are on. - The connector key authenticates the **builder** only. The shipped app needs a server function that holds the key as an environment variable and calls the One API. An app that worked in preview but answers `401` when deployed usually shipped without that variable. The key must never reach the client bundle or a browser `fetch`. - Long CNL workflows can outlast a builder-chat step: ask for them to run in the background. ## 3.13 v0 (Vercel) v0 is a hosted builder: no config file, only the MCP form. Open **+** in the prompt form → **MCPs** → add your own server (the project settings page offers the same under **Add MCP**): | Field | Value | |---|---| | Name | `PlungeAI` | | URL | `https://mcp.plungeai.com/v1` | | Authentication | **Bearer Token** → paste the raw `ozk_` key (v0 adds the `Bearer ` prefix), or **Custom Headers** → `Authorization` = `Bearer ozk_YOUR_KEY`. If one form answers `401`, try the other. `No Auth` and `OAuth` do not apply. | Then pick the server's permission mode: **Disabled**, **Ask for Approval (Manual)** or **Always Run (Auto)**. Manual is the sensible default. **The generated app cannot call MCP.** The builder chat uses MCP; the generated Next.js app calls the One API on the server side only, for example from a Route Handler: ```ts // app/api/plungeai/route.ts export async function POST(req: Request) { const { prompt, agentId } = await req.json() const r = await fetch(`https://api.plungeai.com/v1/agents/${agentId}/execute`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.PLUNGE_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ prompt, sync: true }), }) return Response.json(await r.json()) } ``` Keep the key in a Vercel environment variable for every environment that needs it — never in a client component, never prefixed `NEXT_PUBLIC_`. The route is documented at `https://api.plungeai.com/docs#9-agents-plane`. **Verify:** in the builder chat, the checks in [3.1](#31-before-you-start). **Quirks:** - Previews run without the project's environment variables until the project is linked or deployed; a `401` from a preview app does not mean the MCP integration is wrong. - If generation puts the `fetch` in a `"use client"` component, regenerate with: "PlungeAI calls go through a Route Handler; the key stays server-side." - Long CNL workflows: ask for them to run in the background rather than one long builder-chat call. ## 3.14 Any other MCP client 1. **Native remote / HTTP support (preferred):** URL `https://mcp.plungeai.com/v1`, header `Authorization: Bearer ozk_YOUR_KEY` (or `X-API-Key: ozk_YOUR_KEY`). 2. **stdio-only client:** bridge with `mcp-remote` (Node 18+): ```json { "mcpServers": { "plungeai": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.plungeai.com/v1", "--header", "Authorization: Bearer ozk_YOUR_KEY"] } } } ``` Clients that split spaces inside `args` (mcp-remote's README names Cursor, Codex CLI and Claude Desktop on Windows): pass `"--header", "Authorization:${AUTH_HEADER}"` plus `"env": { "AUTH_HEADER": "Bearer ozk_YOUR_KEY" }` in the same entry. Raise the client's startup timeout if it has one — an `npx` cold start can exceed it. 3. **SSE-only or OAuth-only client** (ChatGPT web connectors, claude.ai custom connectors): not supported yet. Use the One API instead — every capability is reachable over plain HTTPS (`https://www.plungeai.com/docs/one-api`). A config written before 2026-09-20 may point at `https://mcp.plungeai.com/mcp`. That path still answers with the same server, so it keeps working; new configs use `/v1`. ## 3.15 One-click installs **The install page** — `https://www.plungeai.com/install` — has one card per target: Claude Code, claude.ai / Claude Desktop, Cursor, VS Code, Replit, Lovable, Bolt.new, v0, Windsurf, the Ocean CLI and ChatGPT / Codex. Each card lists the steps and, where there is one, a ready config snippet. Type your key into the box at the top first: the **Add to Cursor**, **Add to VS Code** and **Add to Replit** buttons are then rewritten in your browser to carry the `Authorization` header. The page makes no network call with the key; it never leaves the page. **Deeplink redirects** — `GET /install/:tool` answers `302` to the client's install link, **without** a key (you add the header afterwards). It exists for `cursor`, `vscode` and `replit`; any other tool is `404` (live-verified 2026-09-24): ```bash curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://mcp.plungeai.com/install/cursor ``` | Tool | Link | Payload | |---|---|---| | Cursor | `cursor://anysphere.cursor-deeplink/mcp/install?name=plungeai&config=<base64>` | base64 of `{"url":"https://mcp.plungeai.com/v1","headers":{"Authorization":"Bearer ozk_…"}}` | | VS Code | `vscode:mcp/install?<URL-encoded JSON>` | one object: `{"name":"plungeai","type":"http","url":"https://mcp.plungeai.com/v1","headers":{…}}` | | Replit | `https://replit.com/integrations?mcp=<base64>` | base64 of `{"displayName":"PlungeAI","baseUrl":"https://mcp.plungeai.com/v1","headers":[{"key":"Authorization","value":"Bearer ozk_…"}]}` | The `headers` part is present only when the page built the link from a key you typed. **Machine-readable** — `GET /install.json` has the same data: `name`, `mcp`, `api`, `openapi`, `llms_txt`, `skill`, and `tools[]` with `id`, `name`, `method` (`deeplink` · `snippet` · `guide`), `snippet`, `skill`, `steps[]` and, for the three deeplink tools, `deeplink`: ```bash curl -s https://mcp.plungeai.com/install.json ``` **Per-client setup skills** — `GET /connect/<tool>/SKILL.md` returns the `plungeai-in-<tool>` skill as Markdown, for the twelve tools `claude-code`, `claude-ai`, `cursor`, `vscode`, `codex`, `gemini-cli`, `opencode`, `windsurf`, `replit`, `lovable`, `bolt`, `v0`. `GET /connect/SKILL.md` is the client-independent `plungeai-mcp-setup` skill. Paste the URL into your client and ask it to follow the steps: ```bash curl -s https://mcp.plungeai.com/connect/cursor/SKILL.md ``` **Other distribution routes** on the same host (no key needed): | Route | What it is | |---|---| | `/lovable/SETUP.md` | the Lovable connector pack for workspace admins | | `/skill.zip` | the CNL workflow-authoring skill (`plungeai-workflows`), as a zip | | `/skill/<file>` | the files of that skill one by one (`SKILL.md`, `examples/*.yaml`, `references/*.md`) | | `/llms.txt`, `/llms-full.txt` | the same skill as an index and as one bundle, for LLM tools | | `/docs.md` | this whole guide as one Markdown file, for an agent or skill that should read it (the web page is `/docs`) | ## 3.16 The skills catalog `https://skills.plungeai.com` is the catalog of PlungeAI skills — instruction packs your AI client loads so it already knows how to set up and use PlungeAI. A skill is not a connection; it complements one. Version 3.1.0 holds 32 skills (live-verified 2026-10-05): | Group | Skills | |---|---| | Start here (4) | `choose-your-plungeai-door`, `plungeai-mcp-setup`, `plungeai-api-setup`, `plungeai-cli-setup` | | Per client (12) | `plungeai-in-<tool>` for the twelve clients in this chapter | | Capability (13) | `ocean-cli`, `plungeai-platform`, `plungeai-discovery`, `plungeai-tools-connectors`, `plungeai-agents`, `plungeai-missions`, `plungeai-workflows`, `plungeai-results-traces`, `plungeai-memory`, `plungeai-scheduling`, `plungeai-models`, `plungeai-skills-plugins`, `plungeai-campaigns` | | Studio kinds (4) | `workflow`: `plungeai-workflows` (listed under Capability); `agent`: `plungeai-agentic-agent`; `bot`: `plungeai-bot-agent`; `campaign`: `plungeai-campaign-agent` | Each skill is at `https://skills.plungeai.com/<name>/SKILL.md` and `https://skills.plungeai.com/<name>.zip`; all of them are in `https://skills.plungeai.com/plungeai-agent-skills.zip`. `/index.json` lists every skill with its group, `skill_md_url`, `zip_url` and `install_command`; `/llms.txt` is the same for LLM tools: ```bash curl -s https://skills.plungeai.com/index.json ``` **Install today:** download a zip and unzip it into your client's skills folder — each zip holds one folder named after the skill (Claude Code: `.claude/skills/` or `~/.claude/skills/`; claude.ai: upload one single-skill zip at a time under Customize → Skills, not the two `plungeai-in-claude-*` skills and not the bundle, [3.3](#33-claudeai-and-claude-desktop)). **Install once the public repository is published.** The catalog also shows one-command installs from `PlungeAI/plungeai-agent-skills`. That repository is not public yet (live check 2026-09-24); these commands work once it is: ```bash npx skills add PlungeAI/plungeai-agent-skills --all --global # every skill, user-level npx skills add PlungeAI/plungeai-agent-skills --skill plungeai-in-cursor # one skill ``` - Claude Code: `/plugin marketplace add PlungeAI/plungeai-agent-skills`, then `/plugin install plungeai@plungeai`. The plugin brings its own `plungeai` MCP server (header `Bearer ${PLUNGEAI_API_KEY}`): use it instead of `claude mcp add`, not next to it ([3.2](#32-claude-code)). - OpenAI Codex: `$skill-installer PlungeAI/plungeai-agent-skills`. Which skill matches which chapter of this guide: | Skill | Chapter | |---|---| | `plungeai-mcp-setup` | [Authentication & keys](/mcp/authentication), [Errors, feedback & limits](/mcp/errors-limits) | | `plungeai-in-<tool>` | this chapter | | `plungeai-discovery` | [Discovery](/mcp/discovery) | | `plungeai-tools-connectors` | [Tools](/mcp/tools) | | `plungeai-agents`, `plungeai-missions` | [Agents & missions](/mcp/agents-missions) | | `plungeai-workflows` | [Workflows](/mcp/workflows) | | `plungeai-results-traces` | [Runs, results & follow-ups](/mcp/runs-results) | | `plungeai-memory` | [Chat, memory & skills](/mcp/chat-memory-skills) | | `plungeai-scheduling` | [Scheduling](/mcp/scheduling) | | `plungeai-cli-setup`, `ocean-cli` | [3.17 The Ocean CLI](#317-the-ocean-cli) | ## 3.17 The Ocean CLI The Ocean CLI (`ocean-cli`) is a terminal client for the same platform, built for scripts, CI and other agents: every capability is a verb with flags (`ocean-cli workflows run <id>`, `ocean-cli missions run`, `ocean-cli executions list`, …), `--json` on every verb and stable exit codes. `ocean-cli login` signs you in through the browser and stores your `ozk_` key (`--no-browser` asks for it at a hidden prompt instead); `ocean-cli whoami` shows the identity behind the key and `ocean-cli doctor` checks connectivity. The bot surface is there too: `workflows permissions|files|inbox|bindings|save-version`, `bot-groups`, `executions steer|files`, `schedules create --at/--ends-at`, `wake`, `triggers webhook|email|hooks` and `missions run --sync`. It is not an MCP client you configure: it calls this server's `/v1` and the One API with the same key. Its npm package `@plungeai/ocean` is not published yet. `ocean-cli --help` (at every level) lists the commands and flags. ## 3.18 When a client won't connect | Symptom | Cause → fix | |---|---| | `401` on every call | Key mistyped, pasted with whitespace, revoked or expired, or the header never reached the server. Run the `curl` check in [3.1](#31-before-you-start); if it works, the problem is the client config. | | `401` only from one network | The key has an `allowed_ips` fence ([2.5](/mcp/authentication#25-key-fences)). | | `401` only through the bridge | The client split the spaces in `--header` — use the `AUTH_HEADER` form ([3.14](#314-any-other-mcp-client)). | | `405` or an immediate connect error | The client is using SSE (`GET`). Use its HTTP setting (Gemini CLI: `httpUrl`), or the bridge. | | Connected, but no `plungeai_*` tools | The client was not reloaded after the config edit, or its tool cap is full. | | Fewer tools than this guide lists | The key is fenced to a subset of tools — policy, not a fault ([2.5](/mcp/authentication#25-key-fences)). | | The bridge never appears or times out | Node.js is missing, or the `npx` cold start exceeded the client's startup timeout. | | Works for one teammate, not another | Separate keys, separate configs — check each machine. | | `429` with `Retry-After` | Rate limit for your tier ([2.6](/mcp/authentication#26-tiers-and-rate-limits)). | | `⏸ NEEDS APPROVAL` over MCP, or `403 refused` / `409 approval_required` from an app calling the One API | The trust fence, not a connection problem — relay it to the user ([Tools](/mcp/tools)). | More in [Troubleshooting & gotchas](/mcp/troubleshooting). # Protocol & transport Source: https://www.plungeai.com/docs/mcp/protocol Source: `orchestration/mcp-gateway/index.ts`, `server.ts`, `session-do.ts`, `shared.ts`, `tool-outcome.ts`, `execution.ts`, `execution-do.ts`, `leg-state.ts`, `conversation.ts`, `chat.ts`, `tools.ts`, `tool-exec.ts`, `extras.ts`, `orchestration/api-gateway/routes/mcp.ts`, `orchestration/api-gateway/lib/auth.ts`, and the MCP TypeScript SDK 1.29.0 (`node_modules/@modelcontextprotocol/sdk`: `server/index.js`, `server/mcp.js`, `server/webStandardStreamableHttp.js`, `shared/protocol.js`, `types.js`). The PlungeAI MCP server speaks JSON-RPC 2.0 over **Streamable HTTP** at one endpoint: POST https://mcp.plungeai.com/v1 `https://api.plungeai.com/v1/mcp` is the same server, reached through the One API router. The differences at the edge are listed in [4.11](#411-the-two-endpoints-at-the-transport-level). This chapter is for people writing their own client or debugging one. If you use Claude Code, Cursor or another MCP client, the client does all of this for you ([Connecting clients](/mcp/connecting-clients)). ## 4.1 How a request flows The big picture: ``` you type a sentence └─ your client reads it and decides which tool to call └─ POST https://mcp.plungeai.com/v1 (one JSON-RPC message) └─ the platform: registry agents, the CNL engine, your saved workflows ``` Every `POST` passes the same gates, in this order (`index.ts`). The first gate that fails answers, and nothing after it runs: ``` 1. HTTP method GET / DELETE without Mcp-Session-Id → 405 -32000 "Method not allowed." 2. Host not mcp.plungeai.com → 403 -32000 "Invalid Host header: …" 3. Size Content-Length over 1 MiB → 413 -32600 4. Auth X-API-Key, or Authorization: Bearer ozk_… → 401 -32001 (failed auth is metered per IP) 5. Body read with a hard 1 MiB cap → 413 -32600 Rate limit only when the body carries a tools/call → 429 -32000 + Retry-After 6. Parse body is not JSON → 400 -32700 "Parse error" 7. Pre-screen single tools/call: unknown tool / fenced tool → 200 -32602 single tools/call: arguments fail the schema → 200 result, outcome needs_input single initialize: bad params → 200 -32602 "Invalid params: …" 8. Session Mcp-Session-Id present, or a sessions key initializing → session Durable Object (4.5) 9. SDK a fresh MCP server instance answers the message; tools/call streams as SSE ``` What happens after gate 9 depends on the tool. A tool that runs something (a workflow, an agent, a mission) writes a row to your execution history with `source: mcp` and broadcasts it to your other apps. That is the same account and the same history Studio shows. Every authenticated request, including protocol traffic such as `initialize` and `resources/read`, also lands in your activity log ([13.10](/mcp/errors-limits#1310-the-activity-log)). ## 4.2 Endpoint, paths, headers and methods | Item | Value | |---|---| | Endpoint | `POST https://mcp.plungeai.com/v1`. `/v1/` and the pre-2026-09-20 path `/mcp` answer identically. | | Other hosts | Any other Host gets `403`. | | `OPTIONS` | CORS preflight, answered for any origin: `204` with `Access-Control-Allow-Origin: *`, allowed methods `GET, POST, DELETE, OPTIONS`, allowed headers `Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, X-API-Key`, exposed header `Mcp-Session-Id`, max-age 86400 (live-verified 2026-09-24). | | `GET`, `DELETE` | `405` with `Allow: POST, OPTIONS`. They are only accepted with an `Mcp-Session-Id` (opt-in sessions, [4.5](#45-stateless-by-default-opt-in-sessions)). | | Unknown path | `404 {"error":"Not found. MCP endpoint: POST /v1 (Streamable HTTP)."}` (not JSON-RPC). | | No-auth info routes | `GET /v1/info` (server name, version, protocol version, capabilities), `GET /health` (dependency probes; `503` when one fails). | Request headers: | Header | Required | What the server does with it | |---|---|---| | `Authorization: Bearer $PLUNGE_API_KEY` or `X-API-Key: $PLUNGE_API_KEY` | yes | Your `ozk_` key ([Authentication](/mcp/authentication)). Bearer tokens that do not start with `ozk_` are not accepted. | | `Content-Type: application/json` | yes, unless `Accept` is incomplete | When `Accept` lacks one of the two types (below), the server rewrites both headers for you. With a complete `Accept` and another `Content-Type`, the SDK answers `415 -32000 "Unsupported Media Type: Content-Type must be application/json"`. | | `Accept: application/json, text/event-stream` | recommended | The spec requires both types. A request without them is **not** rejected: the server fills them in, so a bare `curl` works. | | `MCP-Protocol-Version` | optional | On requests after `initialize`, an unsupported value gets `400` ([4.3](#43-protocol-version-and-initialize)). It is not checked on `initialize` itself, and a plain `tools/list` answered from the server's cache gets `200` whatever the value. Send a supported version, or leave the header out. | | `Mcp-Session-Id` | only for opt-in sessions | Sending one routes the request to the session host. An unknown id gets `404` ([4.5](#45-stateless-by-default-opt-in-sessions)). | Response types: | Request | Response | |---|---| | `tools/call` (single, or any batch containing one) | `200`, `Content-Type: text/event-stream` ([4.6](#46-toolscall-over-sse-progress-and-keepalive)) | | Every other request | `200`, `Content-Type: application/json` on the default stateless path. On an opt-in session ([4.5](#45-stateless-by-default-opt-in-sessions)) every request, `initialize` included, is answered as SSE. | | Only notifications (no `id`), e.g. `notifications/initialized` | `202 Accepted`, empty body | | An edge gate failed (401, 403, 405, 413, 429, 400) | that status, a JSON-RPC error body ([13.6](/mcp/errors-limits#136-json-rpc-and-http-errors)) | Server info without a key: ```bash curl -s https://mcp.plungeai.com/v1/info ``` A `GET` on the endpoint is refused because the server offers no standalone stream: ```bash curl -s -i https://mcp.plungeai.com/v1 ``` ## 4.3 Protocol version and `initialize` The server implements MCP revision **`2025-11-25`** (`PROTOCOL_VERSION` in `server.ts`). It negotiates exactly as the SDK does (`_oninitialize`): if the `protocolVersion` your client sends is one of `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, `2024-10-07`, the server answers with that version. Anything else gets `2025-11-25`, and your client decides whether it can speak it. `/health` describes this as "negotiates down to 2024-11-05". ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' ``` Ask for an older revision and you get it back: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' ``` The `initialize` result: | Field | Value | |---|---| | `protocolVersion` | the negotiated version (above) | | `serverInfo` | `{ "name": "plungeai.com", "version": "2.7.0" }` (`SERVER_NAME`, `SERVER_VERSION`; the version moves with deploys) | | `capabilities` | `logging: {}`, `resources: {}`, `prompts: {}`, `tools: { listChanged: true }`. The `listChanged` flag comes from the SDK; the stateless server never actually sends a `list_changed` notification ([4.5](#45-stateless-by-default-opt-in-sessions)). | | `instructions` | the server-level usage guide, injected into your client's context | The `instructions` text is the one place that teaches every client how to use the platform. In short, it tells the model to: - relay every successful tool output **verbatim and in full**. The only parts meant for the model are the guidance tails: execution-id footers, poll hints and ⏸ blocks; - treat "my agents" as the user's saved workflows (`plungeai_list_workflows`). `plungeai_list_agents` is the registry of building blocks; - discover first: search the registry in natural language, fetch a full card before first use, and take agent ids only from live results (the catalog is active-only); - run structured agents with the triad `plungeai_list_agents` → `plungeai_get_tool_contract` → `plungeai_execute_tool`, follow the outcome's remediation, never retry blind, and never approve a gated action on its own; - use `mode:"async"` only for long runs and poll `plungeai_get_workflow_status`; - stop at every ⏸ AWAITING USER / AWAITING USER APPROVAL block, relay it, then call `plungeai_continue`; - pass `user_request`, the user's own words, on **every** tool call. Every tool accepts it, and the platform uses it for context, routing and your activity log ([13.10](/mcp/errors-limits#1310-the-activity-log)). `initialize` params are checked before the SDK sees them. A malformed request (for example a numeric `protocolVersion` and no `clientInfo`) gets `-32602` naming the bad fields: `{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params: protocolVersion, clientInfo"},"id":1}` (HTTP 200). This check covers single-message requests only. After `initialize`, a client sends `notifications/initialized`. It has no `id`, so the server answers `202` with no body: ```bash curl -s -o /dev/null -w '%{http_code}\n' https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' ``` **The `MCP-Protocol-Version` header.** You may send it on requests after `initialize`. The SDK accepts any of the five supported versions. An unsupported value gets `400 -32000 "Bad Request: Unsupported protocol version: <v> (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07)"`. Two exceptions: `initialize` itself is never checked, and a single uncursored `tools/list` that a warm server instance answers from its cache (`index.ts`) returns `200` before the check runs, so the same bad header can get `200` or `400` depending on cache state. Leaving it out is fine. ## 4.4 JSON-RPC methods Every method the server answers for a normal key (`server.ts`, SDK `Protocol` and `McpServer`): | Method | Supported | Returns | |---|---|---| | `initialize` | **supported** | [4.3](#43-protocol-version-and-initialize) | | `notifications/initialized` | **supported** | `202`, no body | | `ping` | **supported** | `{}` | | `tools/list` | **supported** | the 22 `plungeai_*` tools in catalog order, or only the tools your key is fenced to. No pagination: there is no `nextCursor`, and a `cursor` param is **ignored** (full list again). Details in [4.10](#410-tool-annotations-read-only-hints-and-metadata) and the [Tool reference](/mcp/tool-reference). | | `tools/call` | **supported** | streamed as SSE ([4.6](#46-toolscall-over-sse-progress-and-keepalive)) | | `resources/list` | **supported** | 5 resources ([Prompts & resources](/mcp/prompts-resources)) | | `resources/templates/list` | **supported** | 3 URI templates | | `resources/read` | **supported** | the resource's text, or `-32002` for an unknown URI | | `prompts/list` | **supported** | 1 prompt, `plungeai` | | `prompts/get` | **supported** | the prompt's message, or `-32602` for an unknown name | | `logging/setLevel` | **partial** | returns `{}`, but the stateless server keeps no state between requests, so the level is not applied to later calls. Progress fallback lines are always sent at level `info` ([4.6](#46-toolscall-over-sse-progress-and-keepalive)). | | `notifications/cancelled` | **ignored** | `202`. The notification arrives in a new request with its own server instance, which cannot reach the call in flight. To stop a run, use `plungeai_executions` `{"action":"cancel"}` ([Runs, results & follow-ups](/mcp/runs-results)). | | `resources/subscribe`, `resources/unsubscribe`, `completion/complete` | **rejected** | `-32601 "Method not found"`. They exist only on the conformance-test keys. | | any other method | **rejected** | `-32601 "Method not found"` | The server never sends requests to your client. Sampling, elicitation and roots are not used by any production tool. A quick liveness check: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":7,"method":"ping"}' ``` **Batches.** A JSON array of messages is accepted and answered as an array. The SDK answers each message on its own: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '[{"jsonrpc":"2.0","id":1,"method":"ping"},{"jsonrpc":"2.0","id":2,"method":"prompts/list"}]' ``` Batch caveats (`index.ts`): - The pre-screen in gate 7 runs for **single** messages only. In a batch, an unknown or fenced tool comes back as the SDK's own `isError` tool result (`MCP error -32602: Tool <name> not found`), and bad arguments come back as the SDK's `Input validation error: …` text. You get no `-32602` envelope and no `needs_input` outcome. Fenced tools are still never run: they are not registered on your server instance. - A batch that contains `initialize` together with other messages gets `400 -32600 "Invalid Request: Only one initialization request is allowed"`. - A batch containing a `tools/call` is answered as SSE. ## 4.5 Stateless by default, opt-in sessions **Stateless is the default.** Each `POST` builds a fresh server instance, answers it, and forgets it (`server.ts`: "Built fresh per request"). No `Mcp-Session-Id` is issued, and you do not need one. Each request is self-contained: `tools/list` or `tools/call` work without an earlier `initialize` in the same connection. Clients still send `initialize` once to learn the version, capabilities and instructions. What statelessness means in practice: | Feature | On the stateless path | |---|---| | Server-initiated stream (`GET`) | not offered: `405` | | `notifications/tools/list_changed` | never sent. If a tool seems missing after a platform update, reconnect your client so it re-reads `tools/list`. | | `logging/setLevel` | accepted, not persisted | | `notifications/cancelled` | cannot reach an in-flight call. Use `plungeai_executions` `cancel`. | | Resumable SSE (`Last-Event-ID`) | not available. A dropped `tools/call` stream is not replayed. The run itself keeps going and can be read with `plungeai_get_result`. | | Sampling, elicitation | not available (and no production tool uses them) | **Opt-in sessions** (`session-do.ts`). A key minted with `sessions: true` gets a stateful session. This is an operator setting: self-service keys from Dashboard → One API → Keys do not carry it. - `initialize` returns an `Mcp-Session-Id` response header. Send it on every later request. - On a session, every `POST` that carries a request is answered as `text/event-stream`, `initialize` and `tools/list` included. When the request speaks `2025-11-25` (the `initialize` param, or the `MCP-Protocol-Version` header after it), the stream opens with a priming event (`id:`, `retry: 1000`, an empty `data:`). Read the `Mcp-Session-Id` response header, then parse the `data:` lines as in [4.6](#46-toolscall-over-sse-progress-and-keepalive). - `GET` with the header opens the standalone SSE stream. It is resumable with `Last-Event-ID`; the server sends a 1000 ms retry hint and stores up to 1,000 replay events per session. - `DELETE` with the header ends the session. - Sessions expire after **10 minutes** without a request. The window slides with each request. - A session is bound to the key that created it. The same id presented by another key, an expired id, an id that never existed, or one longer than 128 characters all get `404 -32001 "Session not found"`. Per the spec, re-initialize to get a new one. - A second `initialize` on a live session gets `400 -32600 "Invalid Request: Server already initialized"`. - Sessions are available on `https://mcp.plungeai.com/v1` only ([4.11](#411-the-two-endpoints-at-the-transport-level)). If your client sends an `Mcp-Session-Id` it picked up elsewhere, the request goes to the session host and gets `404 Session not found`. Drop the header. ## 4.6 `tools/call` over SSE: progress and keepalive A `tools/call` response is a Server-Sent Events stream. Each frame is `event: message`, then one `data:` line holding a JSON-RPC message, then a blank line. Progress notifications (if any) come first. The **last** frame is the JSON-RPC response, and then the stream closes. With curl, keep the `data:` lines: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I"}}}' \ | sed -n 's/^data: //p' ``` Some `tools/call` answers are plain JSON instead of SSE: the gate 7 pre-screen answers (unknown tool, fenced tool, arguments that fail the schema) and every edge error (401, 413, 429). A robust client checks `Content-Type` before parsing. ### Live status: progress notifications While a tool runs, the server streams what it is doing. There is one channel per call, never both (`server.ts` `wrap`): | Your `tools/call` | Channel | Params | |---|---|---| | carries `params._meta.progressToken` (Claude Code and Claude Desktop send one) | `notifications/progress` | `progressToken`, `progress` (strictly increasing), `total` (only when a raw done/total pair passes through unchanged), `message` (the human-readable line) | | no progress token | `notifications/message` (logging) | `level: "info"`, `logger: "<tool name>"`, `data: "<the same line>"` | `progress` is a sequence, not a percentage. The server keeps it strictly increasing as the spec requires, and the readable "5/18" lives in `message`. Notifications stop the moment the call settles. A call that asks for progress (runs a model, costs credits, so it is not auto-verified): ```bash curl -sN https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"_meta":{"progressToken":"p1"},"name":"plungeai_execute_agent","arguments":{"user_request":"one-sentence answer","agent":"one-agent","prompt":"In one sentence: what is a Durable Object?"}}}' ``` Frame shape while it runs (the shape `server.ts` builds; the numbers and text vary): ``` event: message data: {"method":"notifications/progress","params":{"progressToken":"p1","progress":3,"message":"⚙️ Agents working — 0/1 agents complete · 1 active (one-agent)"},"jsonrpc":"2.0"} ``` The lines you will see, by tool (`execution.ts`, `conversation.ts`, `chat.ts`): | Tool | Status lines | |---|---| | workflow, agent, tool and mission runs | `⚙️ Agents working — 5/18 agents complete · ✓ brave-agent · 13 active (tavily-agent, exa-agent…)` · `✍️ Generating answer` · `✍️ Writing (3,516 chars) — …<the last ~72 characters of the answer>` · `⚙️ one-agent still working — 45s elapsed…` · `✅ Complete — 18 agents in 24.6s` | | `plungeai_followup` | `🔎 Looking up the run and checking for an open session…` · `💬 <agent> working — continuing the conversation…` or `📚 No open session — loading the run's stored result as context…` · `⏳ Collecting the answer…` · `💾 Saving this turn to the conversation thread…` · `✅ Complete — continued the existing session` / `✅ Complete` | | `plungeai_continue` | `🔎 Loading the paused conversation…` · `🔓 Approval received` · `💾 Saving this turn…` · `✅ Complete` | | `plungeai_build_workflow` | `📖 Loading the existing workflow…` · `🧠 Generating CNL YAML — the builder picks real agents, validates and self-repairs (30-60s)…` · `🔍 Validating the generated workflow (<n> chars)…` · `💾 Saving "<name>" and syncing to all apps…` · `✅ Complete` | | `plungeai_chat` | `Thinking…` · `Streaming… <n> chars` | | any tool, every 15 s | `⏳ Working — <n>s elapsed` | The 15-second keepalive runs for every tool call. It keeps the SSE stream from going idle long enough for an intermediary to cut it. The `✍️ Writing` lines carry only the tail of the answer as it is produced. The full answer arrives in the final response. Whether you **see** these lines depends on your client. Some render them under the running tool, some ignore both channels. The work and the result are identical either way. `mode:"async"` runs return at once, so they have no live feed ([4.7](#47-async-mode)). ## 4.7 Async mode Four tools can run in the background. The call returns an **execution id** at once instead of waiting for the result: | Tool | `mode` | Default | |---|---|---| | `plungeai_execute_workflow` | `sync` \| `async` | `sync` | | `plungeai_execute_tool` | `sync` \| `async` | `sync` | | `plungeai_run_mission` | `sync` \| `async` | **`async`** (anything but `"sync"`) | | `plungeai_learn` (`action: learn`) | `sync` \| `async` | **`async`** (anything but `"sync"`) | `plungeai_execute_agent` has no `mode`. The immediate answer is an `ok` outcome. Its `structuredContent` has `status: "ok"`, `execution_id`, and a `summary` that says the run started in the background and tells the client to poll `plungeai_get_workflow_status`. The wording differs per tool: "Started execution …" (`plungeai_execute_workflow`), "Started `<name>` …" (`plungeai_execute_tool`), "Started mission …" (`plungeai_run_mission`), "Started distilling …" (`plungeai_learn`). An `ok` outcome with `execution_id` and no `result` means an async start, or a sync run whose output was not yet available ("… completed (execution: …) but results not yet available"). Either way, poll `plungeai_get_workflow_status`, then call `plungeai_get_result`. Don't branch on the summary text. `plungeai_execute_tool` also sets `agent_id` and `operation`. The Markdown text ends with a footer the client acts on: ``` --- execution: `<execution id>` — use this id for plungeai_get_result / plungeai_followup on this run. ``` Then: 1. Poll `plungeai_get_workflow_status` with the id. Its `structuredContent.status` reports the run, and `continuation` is non-null when the run paused for your approval or an answer. 2. When it reads `completed` (or `incomplete`), fetch the output with `plungeai_get_result` ([Runs, results & follow-ups](/mcp/runs-results)). A background run (runs a workflow, costs credits, not auto-verified): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"plungeai_execute_workflow","arguments":{"user_request":"research in the background","mode":"async","format":"json","workflow_yaml":"name: bg-demo\ntasks:\n - id: answer\n agent: one-agent\n prompt: |\n In two sentences: what is edge computing?\n"}}}' \ | sed -n 's/^data: //p' ``` How background runs are hosted (`execution-do.ts`, `leg-state.ts`, `index.ts`): - Each run is held by its own Durable Object in **legs of up to 10 minutes** (the Ten-Minute Relay). A long run hands over at a turn boundary and resumes in the next leg from stored task results and checkpoints. - A leg that is killed (deploy, host restart) is re-entered by a watchdog 12 minutes after it started. If it is killed **3 times in a row** without writing a checkpoint, the run fails with "Run was killed 3 times in a row without yielding a checkpoint…". - A `running` row with no heartbeat for 16 minutes (960 s) is marked failed with `Interrupted (run did not complete)`. A cron does this every 15 minutes, and a status poll does it at once. Its cost is left pending, not `unpriced`: the cost worker then prices whatever the run's agents recorded before it stopped. When to use it: the tool descriptions say async is for runs over about 3 minutes. Most workflows finish in seconds in `sync`. Claude Desktop cuts a single tool call at about 4 minutes, so use async there for anything long. ## 4.8 Output formats: Markdown by default, JSON on request Results come back as **Markdown**, the format an AI reads best: tables, headings, links, step indexes, conversation threads. The server's instructions tell clients to show it verbatim. If your client needs machine-parseable output, add `"format": "json"` to a result-bearing tool. The text becomes a JSON document, and the same object is attached as `structuredContent` (`tool-outcome.ts` `renderAs`). Without `format` (or with `"markdown"`), nothing changes. `isError` is the same in both forms. | Tool | `format:"json"` returns | |---|---| | `plungeai_whoami` | `{ user, auth_type, tier, key_label, key_id, rate_window: { minute_used, day_used } \| null, server }` | | `plungeai_list_workflows` | `{ workflows: [ { id, name, description, kind, folder } ] }`. Without filters this is the recent list; the folder summary line is Markdown-only. | | `plungeai_executions` | `list` → `{ executions: [ { id, input, workflow_name, status, start_time, duration, source } ] }`; `conversation` → `{ execution_id, messages }`; `get` → the status object ([4.9](#49-outputschema-and-structuredcontent)); `output`, `cancel`, `delete` → `{ content: "<markdown>" }` | | `plungeai_list_agents` | `{ content: "<registry markdown>" }` (typed rows are not available yet) | | `plungeai_get_result` | `{ content: "<markdown>" }`, or the outcome object when the answer is guidance (e.g. an unknown id) | | `plungeai_execute_workflow`, `plungeai_execute_agent`, `plungeai_execute_tool`, `plungeai_get_tool_contract` | the outcome object ([4.9](#49-outputschema-and-structuredcontent)) | Tools without a `format` parameter **ignore** it. The server drops unknown arguments before the tool runs. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I, as JSON","format":"json"}}}' \ | sed -n 's/^data: //p' ``` **Content annotations.** Successful Markdown results built as plain text carry `annotations: { "audience": ["user","assistant"], "priority": 0.9 }` (`shared.ts`), a hint that the text is for the user to read as-is. Outcome-envelope results and error text carry no annotations, and neither does `format:"json"` output. ## 4.9 `outputSchema` and `structuredContent` Seven tools declare an `outputSchema` in `tools/list`. For these, `structuredContent` is always present on a non-error result, and it matches the schema: | Tools | Schema | |---|---| | `plungeai_execute_workflow`, `plungeai_execute_agent`, `plungeai_get_tool_contract`, `plungeai_execute_tool`, `plungeai_run_mission`, `plungeai_learn` | the **outcome** envelope (below) | | `plungeai_get_workflow_status` | the **status** object (below) | The outcome envelope (`ToolOutcomeSchema`, deliberately loose so it can grow fields): | Field | Type | Notes | |---|---|---| | `status` | `ok` \| `needs_input` \| `needs_connection` \| `needs_api_key` \| `needs_approval` \| `unavailable` \| `error` | the outcome ([Errors, feedback & limits](/mcp/errors-limits)) | | `summary` | string | always present | | `agent_id`, `operation` | string | when a specific agent answered | | `result` | string | the run's output (on `ok`, or the partial output next to `needs_approval`) | | `execution_id` | string | the run's id, for `plungeai_get_result` / `plungeai_followup` | | `missing` | string[] | absent required fields (`needs_input`) | | `warnings` | string[] | visible notes, e.g. "prose mapped: …" | | `remediation` | `{ message, actions[] }` | what to do next ([13.2](/mcp/errors-limits#132-the-structured-feedback-contract)) | | `contract` | object | the tool contract (from `plungeai_get_tool_contract`, and appended to `needs_input`) | The status object (`plungeai_get_workflow_status`): | Field | Type | |---|---| | `execution_id` | string | | `status` | string (`running`, `completed`, `incomplete`, `failed`, `cancelled`, …) | | `error_message` | string \| null (set when the run failed) | | `workflow_name`, `started_at` (ISO), `final_task_id` | string \| null | | `duration_ms` | number \| null | | `continuation` | null, or `{ status, agent, question: string \| null, pending_action: { summary, price? } \| null }` when paused | `isError` follows the spec: it is `true` only for a terminal `error` outcome and for plain tool failures. The guidance outcomes (`needs_*`, `unavailable`) are answers your client acts on, so they carry `isError: false`. The SDK skips `outputSchema` validation on `isError` results, which is why an error answer from these tools may carry plain text only. Tools without an `outputSchema` still attach `structuredContent` when you ask for `format:"json"` ([4.8](#48-output-formats-markdown-by-default-json-on-request)). ## 4.10 Tool annotations, read-only hints and metadata Six tools carry `annotations: { "readOnlyHint": true }`. They only read. The hint is for your client, for example to run them without a confirmation prompt: `plungeai_get_tool_contract` · `plungeai_list_agents` · `plungeai_list_workflows` · `plungeai_get_result` · `plungeai_get_workflow_status` · `plungeai_whoami` The other 14 tools carry **no** annotations: no `destructiveHint`, `idempotentHint` or `openWorldHint`, so your client applies its own defaults. Some of them change state or spend: the execute and mission tools run work, `plungeai_executions` can delete or cancel, and `plungeai_schedule`, `plungeai_workflow`, `plungeai_memory remember` and `plungeai_templates use` write. Other tool metadata in `tools/list`: - No `title` is set. Clients display the `name`. - The SDK marks every tool `execution: { taskSupport: "forbidden" }`: MCP task-augmented calls are not supported. Use [async mode](#47-async-mode) for long work. - Every tool's `inputSchema` has an optional `user_request` string (up to 4,000 characters) for the user's own words. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' ``` ## 4.11 The two endpoints at the transport level `https://api.plungeai.com/v1/mcp` forwards your JSON-RPC body (plus `Accept`, `Content-Type` and `Mcp-Session-Id`) to this server with your own `ozk_` key, which this server validates again (`orchestration/api-gateway/routes/mcp.ts`). The tools and their answers are identical. The edge is not: | | `https://mcp.plungeai.com/v1` | `https://api.plungeai.com/v1/mcp` | |---|---|---| | Methods | `POST`, `OPTIONS` (+ `GET`/`DELETE` with a session) | `POST` only | | Auth failure | `401` JSON-RPC `-32001` + `WWW-Authenticate` | `401` One API envelope `{"error":{"code":"unauthorized",…}}` + `X-Error-Code: unauthorized` | | Rate limit counts | only requests carrying a `tools/call` | only requests carrying a `tools/call`, counted by this server in the same per-account counter | | Per-key restrictions: tool fence, per-key rate override, sessions | applied | applied. Your key is forwarded and this server validates it again. Sessions need `GET`/`DELETE`, which this route does not offer: open sessions on `mcp.plungeai.com`. | | `plungeai_whoami` shows | `api-key`, your tier and key label | the same | | Other routes | `/log`, `/v1/info`, `/health`, `/install`, `/docs` | `GET /v1/mcp/tools` (a JSON-RPC `tools/list` without writing JSON-RPC, [One API guide](https://api.plungeai.com/docs#11-mcp-plane)) | Use `https://mcp.plungeai.com/v1` when you need sessions. The unauthenticated difference, live-verified 2026-09-24: ```bash curl -s -X POST https://mcp.plungeai.com/v1 -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' echo curl -s -i -X POST https://api.plungeai.com/v1/mcp -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` # Discovery Source: https://www.plungeai.com/docs/mcp/discovery Source: `orchestration/mcp-gateway/server.ts` (`TOOL_DEFS`, `SERVER_INSTRUCTIONS`), `orchestration/mcp-gateway/tools.ts` (`ListAgentsSchema`, `listAgents`), `orchestration/mcp-gateway/tool-exec.ts` (`getToolContract`), `orchestration/mcp-gateway/credential-preflight.ts`, `orchestration/mcp-gateway/registry-cards.ts`, `orchestration/mcp-gateway/extras.ts` (`TemplatesSchema`, `plungeaiTemplates`), `core/core-base/tool-contract.ts`, `orchestration/registry/registry.ts` (search markdown, `KIND_GROUPS`); registry reads live-verified 2026-09-24. Before you run anything, find it. Discovery is three steps. Each one is a read-only tool call that costs no credits: ``` Find ──► plungeai_list_agents search the live registry (active agents only) Inspect ──► plungeai_get_tool_contract typed params, operations, approval gates, YOUR credential status Execute ──► plungeai_execute_tool · plungeai_execute_agent · an agent: task in a workflow ``` `plungeai_templates` (`list` / `get`) is the fourth discovery tool. It lists ready-made workflows. All examples use `https://mcp.plungeai.com/v1`. `https://api.plungeai.com/v1/mcp` is the same server behind the One API ([Overview](/mcp)). Every tool accepts `user_request`, the user's words verbatim. Always send it ([Protocol & transport](/mcp/protocol)). Full parameter tables are in the [Tool reference](/mcp/tool-reference). ## 5.1 Registry agents vs your workflows Two different things are both called "agents". The server's `initialize` instructions teach every client the difference (`SERVER_INSTRUCTIONS` in `server.ts`): | Term | What it is | Tool | The user says | |---|---|---|---| | **registry agent** | A platform building block in the catalog (`brave-agent`, `sec-agent`, `markitdown`, …). Workflows are built from them. | `plungeai_list_agents` | "agents in the registry", "what can the platform do", "which agent should I use for…" | | **your workflows** | Your saved CNL workflows. Users call these "my agents". | `plungeai_list_workflows` ([8.1](/mcp/workflows#81-find-your-workflows--plungeai_list_workflows)) | "show me my agents", "my workflows", "what's in my Finance folder" | An unqualified "show me my agents" means **your workflows**. `plungeai_list_agents` is only for the registry: when the user says registry, discovery or platform agents, or when the client itself is choosing building blocks for a workflow. `plungeai_list_agents` with `kind: "workflows"` searches the registry's public workflow cards. Those are not your workflows. ## 5.2 Search the registry — `plungeai_list_agents` One tool, five modes. The mode is picked from the arguments you send: | You send | Mode | What comes back | |---|---|---| | `search` | Hybrid semantic + keyword search over live cards | The top matches (default **25**) with *Good at* and *Not for → use X instead* for each | | `category` | Filter by registry category | Every active agent in that category (default 25) | | `search` + `category` | Both | Matches inside that category | | `agent_id` | One full card | The complete card markdown ([5.3](#53-read-one-full-card--agent_id)) | | nothing (or only `kind`) | Unfiltered listing | The whole catalog (default **100**), headed `# PlungeAI Agents (live registry)` plus a refine hint | Other arguments: - `kind`: which card set to search. `agents` is the default. See the table below. - `limit`: 1 to 100. It overrides the defaults above. - `format`: `"json"` returns `{"content": "<the same markdown>"}` as text and as `structuredContent`. The source is rendered registry markdown; there are no typed rows yet (tool description, `server.ts`). `search` is semantic, so describe the **capability**, not a name: "web search", "convert pdf to markdown", "post to slack". The registry ranks the results. Trust the ranking. **The catalog is active-only.** For `kind: "agents"` the gateway always adds `status=active` to the registry query (`listAgents` in `tools.ts`). Every agent it lists is deployed and callable. The execution tools refuse any id that is not `status:active` ([Tools](/mcp/tools), [Agents & missions](/mcp/agents-missions)). So take agent ids from a live search, never from memory, another platform, or a guess. ### Try asking - *"What agents do you have for financial analysis and SEC filings?"* - *"Which agent should I use to turn a PDF into markdown?"* The search is semantic. For this one you get `pdf-worker`, `markitdown` and `ms-office-worker`, and each card says what it is *not* for (`pdf-worker` → "Extracting text from existing PDFs → use `doc-converter`"). Live-verified 2026-09-24. - *"What connectors are available?"* Also try personas, experts, skills and models. - *"What agents are in the registry for web search?"* ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_list_agents","arguments":{"user_request":"Which agent should I use to turn a PDF into markdown?","search":"convert pdf to markdown","limit":3}}}' ``` The result `text` is the registry's own markdown, passed through unchanged. An abridged excerpt (live registry, 2026-09-24): ```markdown # Registry search — q="convert pdf to markdown" *3 results* ## PDF Worker (`pdf-worker`) *agent · tools · status:active* Generates PDF documents from Markdown content. … **Good at:** - Converting meeting notes in Markdown to a shareable PDF document (exportToPdf) … **Not for:** - Extracting text from existing PDFs → use `doc-converter` (pdf-worker generates PDFs; it does not parse or extract content from existing PDF files) … ## markitdown (`markitdown`) *agent · tools · status:active* … ``` Each result has a heading with the name and id, a line with type, category and status, the description, **Good at:** and **Not for:**. The `*N results*` line counts the results on this page, not the catalog total. > **Not-for redirects come from the card as written.** A redirect target is not checked against the active catalog. On 2026-09-24, `sec-agent`'s card said "Real-time stock quotes → use `alpha-vantage`", and `alpha-vantage` is `status:testing`, so execution refuses it. Before you use a redirect target, check it with `search` or `agent_id`. ### Kinds `kind` picks the card set (`KIND_GROUPS` in `orchestration/registry/registry.ts`): | `kind` | Searches | Active-only filter | |---|---|---| | `agents` (default) | Callable agents, the ids that go into a workflow's `agent:` | **supported**. Always applied | | `personas` | Digital twins (the `persona` of a `one-agent` call, see [Agents & missions](/mcp/agents-missions)) | not applied (these cards carry no `status:active` tag) | | `experts` | Expert / specialist cards for mission capability injection | not applied | | `skills` | Skill cards (`skills:` on a harness task) | not applied | | `models` | Model cards (ids like `anthropic:claude-haiku-4-5-20251001`) | not applied | | `connectors` | Connector cards | not applied | | `workflows` | Public registry workflow cards, **not your workflows** | not applied | The status filter applies only to agents on purpose. Almost no card in the other sets carries `status:active` (personas, experts and workflows none, skills 1 of 1,053 when the rule was written), so filtering would empty the list (comment in `listAgents`). ### Categories On 2026-09-24 the live catalog held **92 active agents in 14 categories**: `ai`, `blockchain`, `coding-agents`, `communication`, `ecommerce-agents`, `entertainment`, `financials`, `google-workspace`, `media`, `microsoft-365`, `payment`, `search`, `shopping`, `tools`. Category values must match exactly. The finance category is `financials`: `category: "finance"` returns `*0 results*` (live-verified 2026-09-24). If you are unsure, use `search` ("agents for financial analysis") and let the ranking work. For counts per category, read the `plungeai://agents/categories` resource ([Prompts & resources](/mcp/prompts-resources)). > **Don't ask for "all agents".** An unfiltered listing is large. On 2026-09-24 the full active catalog in markdown was about **150 KB**, all of it going into your client's context in one call. Always filter by `search`, `category` or `kind`. ### When the registry does not answer | Answer | Meaning | `isError` | |---|---|---| | `Registry search failed (<status>).` | The registry answered with a non-OK status | true | | `Registry unavailable — try again shortly.` | The call threw (timeout 10 s) | true | | `Registry service not available.` | The gateway has no registry binding | true | These are plain text errors, not outcome envelopes. `plungeai_list_agents` has no `outputSchema`. Retry after a few seconds. ## 5.3 Read one full card — `agent_id` `agent_id` returns the agent's complete card as markdown (`GET /api/cards/agent/<id>?fields=full` on the registry). It includes the description, *Best for*, the *Not for* table, the prompt format, operations, parameters, examples, output, credentials, demonstrations and tags. Fetch it before the first use of an unfamiliar agent. Cards are several KB each (`weather-agent`: about 12.7 KB, live 2026-09-24). `agent_id` takes precedence: `search`, `category` and `kind` are **ignored** when `agent_id` is set. It always reads an **agent** card. *Try asking: "Show me the full card for sec-agent — what operations does it support?"* ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_list_agents","arguments":{"user_request":"Show me the full card for the weather agent","agent_id":"weather-agent"}}}' ``` A wrong or inactive id is not an error. The answer is guidance with live near-matches, and `isError` stays false: | Case | Answer (from `listAgents`) | |---|---| | No card with that id | "No registry card with id `<id>` exists — ids cannot be guessed or carried over from other platforms (this one may be stale). Nothing failed; pick a live id below or refine with `search`." Then `## Closest live matches` (the top 3 active cards) | | Card exists but is not `status:active` | "`<id>` is not available over MCP — only `status:active` agents are listed and callable here. Nothing failed; pick a live agent below or refine with `search`." Then `## Closest live matches` | The MCP server never shows a non-active card, not even when you ask for it by id. ## 5.4 The invocation contract — `plungeai_get_tool_contract` Structured agents are *forms, not chats*. A card with a Parameters table (`markitdown`, `weather-agent`, the Gmail agent, …) takes typed fields, not a sentence. `plungeai_get_tool_contract` gives your client the exact form. It lists every typed field, the operations and which of them need approval, worked examples, and the **live credential status for your account**. Then the client fills the form and calls [`plungeai_execute_tool`](/mcp/tools). The contract *is* the API documentation for that agent. You can also just ask about an agent in plain language. The answer comes from its card (what it is good at, what to use instead) and its contract (the exact fields, and whether you are set up): - *"Before I use the Gmail agent — what exactly does it need from me, and am I connected?"* - *"What is the x402 broker for?"* - *"Can I search Amazon and pay with x402? What information do you need from me?"* ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_get_tool_contract","arguments":{"user_request":"What does the weather agent need from me?","agent_id":"weather-agent"}}}' ``` ### What comes back The `text` is Markdown in this order (`renderContractMarkdown` in `tool-contract.ts`, then `getToolContract` in `tool-exec.ts`): 1. ``# <Name> (`<id>`)`` and the description. 2. `## Operations`: one line per operation, with its purpose, `(requires: …)` for fields that only that operation needs, and `⚠️ GATED: requires explicit user approval` on gated operations. 3. `## Parameters`: one line per field, with its type, `required`, `one of: …` (enums read from the card), `default: …` and the description. 4. `## Examples`: up to three worked CNL snippets from the card's demonstrations. 5. `**Returns:**`: the card's output description. 6. `## Credentials`: your live status (table below). 7. A closing line: `Run it: plungeai_execute_tool {agent_id: "<id>", operation: "<one of the operations above>", params: {<the parameters above>}}. Gated operations pause for user approval — relay the ⏸ block and continue with plungeai_continue.` The hint is the server's own text. A gated floor operation run through `plungeai_execute_tool` comes back as a trust-fence refusal that `plungeai_continue` cannot resume; it runs only from Ocean Studio ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)). An excerpt for `weather-agent` (rendered from the live card, 2026-09-24): ```markdown # Weather Agent (`weather-agent`) Complete OpenWeatherMap client: current conditions, 5-day/3-hour forecast, … EVERY location-based operation accepts either lat/lon coordinates OR a `location` city name … ## Operations - `current` — Real-time weather conditions - `forecast` — 5-day forecast with 3-hour intervals - `air-quality` — Current AQI (OWM 1-5 scale) + pollutant components - `air-quality-history` — Historical AQI between start and end (requires: start, end) - `one-call-timemachine` — Weather at a past/future moment (requires: dt) … ## Parameters - `lat` (number): Latitude - optional when `location` is given (auto-geocoded) - `location` (string): City name auto-geocoded to coordinates, e.g. "Hamburg,DE" or "Sun Valley,ID,US" … - `units` (string): `standard` (Kelvin) | `metric` (Celsius, default) | `imperial` (Fahrenheit) - `start` (number): Unix start time — (operation: air-quality-history, required there) … ## Credentials - 🔐 Platform-managed — nothing for the user to connect. ``` `structuredContent` is the outcome envelope ([6.4](/mcp/tools#64-the-outcome-vocabulary)) with the contract attached: ```json { "status": "ok", "agent_id": "weather-agent", "summary": "Contract for weather-agent", "contract": { "agent_id": "weather-agent", "name": "Weather Agent", "description": "…", "operations": [{ "name": "air-quality-history", "purpose": "…", "gated": false, "required_params": ["start", "end"] }], "inputSchema": { "type": "object", "properties": { "location": { "type": "string", "description": "…" } }, "required": [], "additionalProperties": true }, "examples": [{ "title": "Current weather by city name (no coordinates)", "cnl": "agent: weather-agent\noperation: current\n…" }], "output_type": "…", "credentials": [] } } ``` With `format: "json"` the text becomes this same object as a JSON document. The Markdown `## Credentials` lines are then not in the text, but `contract.credentials` carries the same data. ### Contract fields | Field | Meaning | |---|---| | `agent_id`, `name`, `description` | The card's identity | | `operations[]` | `{ name, purpose?, gated, required_params? }`. The values for `operation` on `plungeai_execute_tool`. Built from the card's Operations table, its per-operation parameters and its demonstrations. An empty list means no operation selector is known, not "refuse" | | `inputSchema` | JSON Schema for `params`: `type: object`, `properties`, `required`, `additionalProperties: true` (fields not on the card are legal and pass through) | | `examples[]` | `{ title, cnl }`: up to three copy-paste CNL tasks | | `output_type` | What the agent returns, in prose | | `soleRequiredParam` | Present when exactly one string field is required. A prose `prompt` can be mapped onto it ([6.3](/mcp/tools#63-typed-params-prose-and-operations)) | | `credentials[]` | `{ provider, tier, required, connected, status, accountEmail }` per requirement. `connected: null` means the status could not be checked | ### How the contract is built The card's parameter types are free text written by people. The generator never throws: an unknown type becomes `string`, and the raw type is kept in the description (`buildToolContract` in `tool-contract.ts`). These rules shape what your client sees: - **Envelope names are skipped.** `agent`, `type`, `id`, `operation`, `depth`, `user_id`, `executor_user_id`, `workflow`, `execution_id` belong to the task envelope. They never appear as params. The operation goes in `plungeai_execute_tool`'s own `operation` argument. - **Credential fields are never required.** `access_token`, `api_key`, `apikey`, `bearer_token`, `oauth_token`, `refresh_token` are shown with "(platform-injected from your connected account at runtime — do NOT supply)". - **Operation-specific requireds** live on `operations[].required_params` and are enforced only when you run that operation. - **`prompt` as an alias.** When a card marks `prompt` and another text field required together (for example `query` + `prompt`), `prompt` is made optional. - **Old multi-operation cards.** On a card with two or more operations and no per-operation attribution, the fields it marks required are moved into the description as "(required by some operations — see the examples)". Otherwise every call would be refused for fields that belong to other operations. - **Gated operations** are the platform floor list (`buy`, `purchase`, `fetch_paid`, `send`, `transfer`, `pay`, `send_payment`, `withdraw`, `shop`) plus any `gated:<op>` tag on the card. See [The trust fence](/mcp/tools#66-the-trust-fence--needs-approval). ### Credentials section The `## Credentials` lines come from the agent's credential requirements ([6.7](/mcp/tools#67-connected-accounts-and-your-own-keys)) and a live status lookup for the calling user: | Line | Meaning | |---|---| | `🔐 Platform-managed — nothing for the user to connect.` | The agent runs on platform keys | | `` `<provider>`: 🔐 platform-managed. `` | This requirement is served by the platform | | `` `<provider>` (private connector): ✅ connected as you@… `` | Your OAuth account is connected. The email is shown when known | | `` `<provider>` (bring-your-own key): ⚠️ NOT connected — required; execution will stop with connect instructions until the user connects it at https://studio.plungeai.com → Connectors. `` | Missing. `expired — reconnect` replaces `NOT connected` for an expired connection; `(optional)` replaces `— required; …` for an optional one | | `` `<provider>` (<tier>): status could not be checked — execution will verify. `` | The lookup failed. It fails open, and the execution checks again | ### Outcomes | `status` | When | `isError` | |---|---|---| | `ok` | The contract was built | false | | `unavailable` | No card with that id (``No registry card with id `<id>` exists — … Nothing was executed.``), or the card is not `status:active`. Up to three live alternatives are included as `use_alternative` actions | false | | `error` | The registry did not answer: `The registry did not answer — the contract cannot be fetched right now.` with "Retry in a few seconds." | true | The tool is read-only (`readOnlyHint: true`). Card lookups are cached for 60 seconds per gateway isolate (`registry-cards.ts`), so an agent that was just deactivated can still look active for up to a minute. ## 5.5 Workflow templates — `plungeai_templates` `list` and `get` Templates are a platform-wide library of ready-made workflows. They are the same rows Studio's gallery shows (`templates` / `template_versions` in D1, content in KV). Three actions: | `action` | Needs | Returns | |---|---|---| | `list` | optional `category`, `limit` (1–100, default 50) | `# Workflow Templates (N)`: a table with Name, Kind, Category, Difficulty, Used, ID, Description (first 90 chars). Only `status = 'active'` workflow templates, featured first, then display order, then most used | | `get` | `template_id` | The name, description, `**Category:** … · **Difficulty:** … · **Used:** N×` and the full YAML in a code block | | `use` | `template_id`, optional `name`, `folder` | Copies the template into your account as a new workflow. See [8.9](/mcp/workflows#89-start-from-a-template--plungeai_templates-use) | *Try asking: "What workflow templates are available? Start one from the due-diligence template."* The client lists the templates, finds the due-diligence one, then calls `use` ([Workflows](/mcp/workflows)). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_templates","arguments":{"user_request":"What workflow templates are available?","action":"list","limit":10}}}' ``` To read one template's YAML, pass its id from the list: ```bash # get needs a real template id from the list above curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"plungeai_templates","arguments":{"user_request":"Show me the due-diligence template","action":"get","template_id":"<template id>"}}}' ``` | Answer | Meaning | `isError` | |---|---|---| | `No active workflow templates.` | `list` matched nothing (for example an unknown `category`) | false | | `template_id is required for this action.` | `get` or `use` without an id | true | | `Template not found: <id>` | No active template with that id | true | | `Template content missing from storage.` | The row exists but its YAML is gone | true | `plungeai_templates` is not marked read-only, because `use` writes a workflow. `list` and `get` change nothing. The tool has no `format` argument; its output is always Markdown. ## 5.6 Other discovery surfaces - **Resources.** `plungeai://agents/list`, `plungeai://agents/categories`, `plungeai://personas/list` and the `plungeai://agents/{category}` template read the same live registry (agents active-only). `plungeai://docs/workflow-authoring` is the full CNL authoring guide. See [Prompts & resources](/mcp/prompts-resources). - **The `/plungeai` prompt** gives the client the whole discover → inspect → execute routine in one message ([Prompts & resources](/mcp/prompts-resources)). - **Over HTTP.** The One API has the same catalog as REST routes: `GET /v1/discovery/search` and `GET /v1/tools/{id}` (https://api.plungeai.com/docs#8-discovery, https://api.plungeai.com/docs#6-tools-plane). - **Skills for your client.** https://skills.plungeai.com has installable skills that teach Claude Code, Cursor and other clients these same patterns ([Connecting clients](/mcp/connecting-clients)). # Tools Source: https://www.plungeai.com/docs/mcp/tools Source: `orchestration/mcp-gateway/tool-exec.ts` (`ExecuteToolSchema`, `executeTool`), `orchestration/mcp-gateway/tool-outcome.ts` (`ToolOutcome`, `renderOutcome`, `pauseOutcome`, `classifyExecutionError`), `orchestration/mcp-gateway/credential-preflight.ts`, `orchestration/mcp-gateway/registry-cards.ts`, `orchestration/mcp-gateway/execution.ts`, `orchestration/mcp-gateway/execution-do.ts`, `orchestration/mcp-gateway/conversation.ts`, `orchestration/mcp-gateway/server.ts`, `orchestration/mcp-gateway/index.ts` (argument pre-screen), `core/core-base/tool-contract.ts`, `core/core-base/call-agent.ts` (`ALWAYS_GATED`), `orchestration/cnl-engine/dispatch.ts` (`gatedDispatchOp`); live registry cards read 2026-09-24. A **structured tool-agent** is a registry agent whose card has a Parameters table: `weather-agent`, `markitdown`, `google-gmail`, `resend`, `x402-wallet-agent`, … It takes typed fields, not a sentence. `plungeai_execute_tool` runs one operation of one such agent. It uses the same engine pipeline as every other execution tool, so the run lands in your execution history, shows in Studio's sidebar, and can be followed up. Every answer is a structured **outcome**, never a raw error. ``` Find ──► plungeai_list_agents the agent id (Discovery) Inspect ──► plungeai_get_tool_contract fields, operations, gates, your credentials Execute ──► plungeai_execute_tool pre-flight checks → engine → outcome ``` Base URL `https://mcp.plungeai.com/v1`. `https://api.plungeai.com/v1/mcp` is the same server; the One API also has a REST twin of this chapter (https://api.plungeai.com/docs#6-tools-plane). Always pass `user_request` ([Protocol & transport](/mcp/protocol)). Parameter types and limits are in the [Tool reference](/mcp/tool-reference). ## 6.1 Run a tool — `plungeai_execute_tool` | Argument | Meaning | |---|---| | `agent_id` | The registry agent id (required). Take it from [`plungeai_list_agents`](/mcp/discovery) | | `operation` | One of the contract's `operations[].name`. Leave it out only for agents without operations | | `params` | The typed fields, exactly as the contract's `inputSchema` names them | | `prompt` | Free text. It satisfies missing requireds on prompt-capable cards, and in one narrow case is mapped onto the only required field ([6.3](#63-typed-params-prose-and-operations)) | | `mode` | `sync` (default) or `async`: return an execution id at once and run in the background | | `format` | `markdown` (default) or `json` ([Protocol & transport](/mcp/protocol)) | Your client usually does this for you. It fetches the contract, fills the form, then calls the agent. You just ask. *Try asking: "Give me a five-day weather forecast for Sun Valley, including air quality."* The client runs `weather-agent` with typed fields, by place name, with no coordinates needed. The weather agent covers the full OpenWeather surface: current conditions, hourly and daily forecasts, air quality, geocoding and more. Here that means two calls, `forecast` and `air-quality`, or one `one-call`. This call runs a real agent and writes an execution to your history (not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_tool","arguments":{"user_request":"Give me a five-day weather forecast for Sun Valley, including air quality.","agent_id":"weather-agent","operation":"forecast","params":{"location":"Sun Valley,ID,US","units":"imperial"}}}}' ``` A `tools/call` answers as a `text/event-stream`: live progress frames first, then the result ([4.6](/mcp/protocol#46-toolscall-over-sse-progress-and-keepalive)). On success the result `text` is the agent's own output, followed by the execution footer: ```markdown …the forecast, as the agent rendered it… --- execution: `<execution id>` — use this id for plungeai_get_result / plungeai_followup on this run. ``` `structuredContent` carries the envelope: ```json { "status": "ok", "agent_id": "weather-agent", "operation": "forecast", "summary": "`weather-agent · forecast` completed.", "result": "…", "execution_id": "<execution id>" } ``` The run appears in your history as `weather-agent · forecast`. Its title is your `user_request` (or the prompt, or the params with secrets redacted, at most 500 characters). ## 6.2 What happens on a call The order is fixed (`executeTool` in `tool-exec.ts`). Steps 1–4 can refuse the call, and a refusal there is **not a run**: nothing is dispatched and no execution is written. 1. **Argument check.** The gateway validates the arguments against the tool's `inputSchema` before the tool runs (`validateToolArgs`, `index.ts`). A missing `agent_id` or an out-of-range value returns `needs_input` ("`plungeai_execute_tool` was not called — its arguments do not match the tool's schema.") with the fields named. 2. **Card fence (active-only).** An unknown id returns `unavailable` ("No registry card with id `<id>` exists — agent ids cannot be guessed or carried over from other platforms. Nothing was executed."). An inactive card returns `unavailable` ("`<id>` is registered but not `status:active` — only active agents are callable over MCP."). Both include up to three live alternatives. If the registry itself does not answer, the call proceeds (fail-open) with a thin contract, and the engine is the backstop. 3. **Contract validation.** Params whose names belong to the task envelope (`agent`, `type`, `id`, `operation`, `depth`, `user_id`, `executor_user_id`, `workflow`, `execution_id`) are **rejected** with `needs_input` "These params collide with task-envelope fields and cannot be passed". Missing required fields (global, plus the chosen operation's `required_params`) are rejected with `needs_input` "Required parameters are missing. Nothing was executed.", with the full contract attached. The missing-field check is skipped when you send a `prompt`. 4. **Credential pre-flight.** If the agent needs one of your connected accounts or your own key and it is not connected, expired or in error, the answer is `needs_connection` or `needs_api_key` with the exact thing to connect ([6.7](#67-connected-accounts-and-your-own-keys)). This check fails open: if the status lookup fails, the call proceeds. 5. **Selector fill and prose mapping** ([6.3](#63-typed-params-prose-and-operations)). 6. **Execution row.** A row opens in your history (`workflow_executions`, status `running`). 7. **Dispatch.** `mode: "async"` hands the run to its execution Durable Object and returns at once ([6.8](#68-long-runs-mode-async)). Sync runs the engine and streams progress. 8. **Engine fence.** An outward, irreversible operation is refused here, before the agent is called ([6.6](#66-the-trust-fence--needs-approval)). 9. **Outcome.** A failure is classified into the outcome vocabulary ([6.4](#64-the-outcome-vocabulary)). A success is checked for a pause (approval or question), then returned. The classified status is also written to the execution's `outcome` column, so dashboards and D1 agree with what your client saw. ## 6.3 Typed params, prose and operations Cards are written by people and under-document. The gateway smooths five things. All but the first add a `⚠️` warning line to a successful answer (`warnings[]` in `structuredContent`): - **Operation selector.** The envelope carries `operation`, but many agents read their operation from a param named `action`, `op` or `operationType`, and silently run their default when it is absent. If the card declares one of these params and you did not set it, `operation` is copied into it. For example, `resend` reads `action`. - **Prose onto the one required field.** If you send *only* a `prompt` (no params), and the card's requireds come down to exactly **one** free-text string field, the prompt is passed as that field. A report generator whose only required field is `content` is one example. You then get the warning "prose mapped: your prompt was passed as `<field>` (this card's only required parameter). If that is not what you meant, retry with typed params." The prompt is never mapped into an operation selector, an enum field, or a URL field (`action`, `op`, `operation`, `operationtype`, `function`, `endpoint`, `url`, `uri`, `link`): a sentence is not an action name and not a URL. - **Prose on a multi-operation card.** A structured agent does not interpret a prompt. Without `operation` it runs its **default** operation, which reads as success while ignoring the request. So you get the warning "structured agent: the prose prompt is not interpreted — without `operation` it ran its default operation. For anything else, pick an operation from the contract and pass typed params." - **Unknown operation.** An operation that is not on the card is **not rejected**; the call proceeds with a warning, because cards can under-document. - **Extras and aliases.** Params not on the card are passed through verbatim (`params not on the card (passed through verbatim): …`). When a card marks two alternative fields both required (`query` *and* `prompt`), filling one is enough (`card also marks … required — likely alias/alternative fields; proceeding with what you provided.`). If a form is still missing something, the answer names the exact fields and shows the form. It never comes back as a bare error, so *"do what it suggests"* is always enough. ## 6.4 The outcome vocabulary Every `plungeai_execute_tool` answer, and every other execution tool's answer, is one `ToolOutcome` (`tool-outcome.ts`). It is sent as Markdown `text` plus the same object as `structuredContent`, and the tool's `outputSchema` declares it: ```json { "status": "ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error", "summary": "…", "agent_id": "…", "operation": "…", "result": "…", "execution_id": "…", "missing": ["…"], "warnings": ["…"], "remediation": { "message": "…", "actions": [ { "action": "…" } ] }, "contract": { } } ``` | `status` | Header in `text` | `isError` | Produced when | What to do | |---|---|---|---|---| | `ok` | (the result itself) | false | The run finished. An `async` start is also `ok` | Show the result. Keep the execution id | | `needs_input` | `📝 NEEDS INPUT` | false | A field is missing, malformed or reserved; the agent rejected its inputs (the contract is attached); the run paused with a question (`⏸ AWAITING USER: …`) | Fix the call from the contract, or relay the question and answer with `plungeai_continue` | | `needs_connection` | `🔌 NEEDS CONNECTION` | false | A required connected account is missing or expired (pre-flight, nothing run); the upstream API answered 401; the account lacks a license; the OAuth token lacks scopes | The **user** connects the service in Ocean Studio → Connectors, then the client retries the identical call | | `needs_api_key` | `🔑 NEEDS API KEY` | false | A bring-your-own-key agent has no key saved for this user (pre-flight); a run named a missing API key or developer token | The user saves the key once in Ocean Studio → Connectors, then the client retries | | `needs_approval` | `⏸ NEEDS APPROVAL` | false | An outward, irreversible operation was refused by the engine; or the run staged an action and paused (`⏸ AWAITING USER APPROVAL: …`) | Relay it to the user verbatim; never approve on your own. A staged action (an `approve_via` action) continues with `plungeai_continue {approve: true}` after the user's yes. An engine refusal (no actions) cannot be continued: it runs only from Ocean Studio ([6.6](#66-the-trust-fence--needs-approval)) | | `unavailable` | `🚫 UNAVAILABLE` | false | The id is unknown or not active; the agent is registered but not deployed (a platform gap, not your mistake); the agent reported a temporary condition itself (quota, upstream down) | Pick one of the alternatives in the answer | | `error` | `❌ ERROR` | **true** | A genuine failure, with the real reason: the run timed out; the agent failed; a transient platform error | Fix the input and rerun, or relay the reason. Don't keep polling | `isError` is true only for `error`. The guidance statuses are answers your client acts on, not protocol failures (MCP Tools → Error Handling). JSON-RPC protocol errors are kept for the edge: authentication, rate limits, unknown tools and malformed requests ([Errors, feedback & limits](/mcp/errors-limits)). A non-`ok` answer is rendered in this order (`renderOutcome`): ```markdown 📝 NEEDS INPUT — `weather-agent` air-quality-history Required parameters are missing. Nothing was executed. Missing required: `start`, `end` **To proceed:** Fill the missing fields (contract below) and retry the same call. - Provide `start` — Unix start time — (operation: air-quality-history, required there) and retry the same call. - Provide `end` — Unix end time — (operation: air-quality-history, required there) and retry the same call. --- # Weather Agent (`weather-agent`) …the full contract… _This is structured guidance, not a crash — do not retry the identical call. Follow the remediation, or relay it to the user._ ``` That is the real answer for `{"agent_id":"weather-agent","operation":"air-quality-history","params":{"location":"Sun Valley,ID,US"}}` (rendered from the live card, 2026-09-24). It is refused before dispatch, so it is free and writes no execution. When the outcome carries an execution id, an `_(execution: …)_` line comes before the last line. Warnings follow at the end as `⚠️` lines. **A refusal is not a run.** The argument check, the card fence, contract validation and the credential pre-flight all answer before an execution exists, so your history stays clean. From step 6 on, an execution row exists, even when the outcome is `needs_input` (the agent rejected its inputs), `needs_approval` (the engine fence) or `error`. ### Classification of run failures When a run fails inside the engine, `classifyExecutionError` maps the raw error, in this priority order: 1. An explicit `[outcome:<status>]` tag from the agent or engine wins. The tag is stripped from the text. 2. A declared `needs_approval` becomes the trust-fence answer ([6.6](#66-the-trust-fence--needs-approval)). 3. "requires a valid license" → `needs_connection` (an external license constraint: the connected tenant needs a license from its admin; not a wrong call). 4. `ACCESS_TOKEN_SCOPE_INSUFFICIENT` / "insufficient authentication scopes" → `needs_connection`: reconnect in Studio to grant more scopes. 5. A named missing key ("… API key not configured", "No … API key available") or `developer_token` → `needs_api_key`. 6. `401`, "invalid authentication", `UNAUTHENTICATED` → `needs_connection`, or `needs_api_key` when the agent declares a bring-your-own key and no OAuth connector. The provider is named when the agent declares one (only `plungeai_execute_tool` passes the declared credentials to the classifier). 7. "service binding … not found" → `unavailable` ("registered but not deployed … a platform-side gap"). 8. Bad-input phrasing ("missing", "invalid parameters", "please provide", "unknown operation", …) → `needs_input` with the contract attached. 9. A timeout → `error` with `Retry with {"mode":"async"} — then poll plungeai_get_workflow_status.` 10. Anything else → `error`: `Execution failed: <the real message, up to 800 characters>`. Two guarantees come with it. Limits are told to you, never silently applied: search agents declare their maximum query length on the card (for example `brave-agent`: "Maximum: 400 characters — over-limit queries refused"), and an over-limit query comes back as `needs_input` instead of being truncated. Provider failures surface with the real reason: a provider stream that fails leaves an error record, so the run reports the provider's error instead of waiting out a streaming timeout (`execution.ts`). ## 6.5 Remediation actions `remediation.actions[]` is machine-readable. Each action also renders as one bullet under **To proceed:**: | `action` | Fields | Rendered as | |---|---|---| | `provide_field` | `field`, `description?` | ``Provide `<field>` — <description> and retry the same call.`` | | `connect_provider` | `provider`, `url`, `scopes?` | `Connect **<provider>**: open https://studio.plungeai.com → Connectors and connect the account (scopes: …). Relay this to the user — it needs their browser.` | | `provide_api_key` | `provider`, `how` | `Provide a **<provider>** API key: Save your <provider> credential in Ocean Studio (https://studio.plungeai.com) → Connectors, then retry this exact call.` | | `approve_via` | `tool: plungeai_continue`, `execution_id` | `Ask the user to approve, then call plungeai_continue {execution_id: "<id>", approve: true}. NEVER approve on your own.` | | `respond_via` | `tool: plungeai_continue`, `execution_id`, `question?` | `Relay the question to the user ("<question>"), then call plungeai_continue {execution_id: "<id>", message: "<their answer>"}.` | | `retry_with` | `changes`, `note?` | `Retry with {"mode":"async"} — then poll plungeai_get_workflow_status.` | | `use_alternative` | `agent_id`, `note?` | ``Alternative: `<agent_id>` — <first 120 chars of its description>`` | The rule for clients, which the server also sends in its `initialize` instructions: **follow the remediation, never retry the identical call blind, and never approve a gated action on your own.** For `needs_connection` and `needs_api_key`, the step needs the user's browser. Relay it, wait for them to confirm, then retry the *identical* call. Your client usually self-corrects on `needs_input`: *"do what it suggests"* is enough. ## 6.6 The trust fence — NEEDS APPROVAL Some operations are outward and irreversible: they move money or cause real-world side effects. The platform will not run them unattended. Two mechanisms produce `needs_approval`. **1. The engine floor refuses outright.** The floor list is fixed in code and no YAML, prompt or card can remove it (`ALWAYS_GATED`, `core/core-base/call-agent.ts`): `buy` · `purchase` · `fetch_paid` · `send` · `transfer` · `pay` · `send_payment` · `withdraw` · `shop` At every deployed-agent dispatch, the engine reads the task's `operation` (or `action`). If it is on the list, the engine refuses before the agent is called (`gatedDispatchOp`, `orchestration/cnl-engine/dispatch.ts`). The MCP answer: ```markdown ⏸ NEEDS APPROVAL — `resend` send This operation is outward and irreversible (it can move money or cause real-world side effects) and will NOT run unattended. operation "send" on resend is gated (outward/irreversible) and requires human approval — it cannot run unattended. **To proceed:** Relay this to the user and get their explicit go-ahead before it runs. Never approve it on your own. To perform it, run it from an interactive session (Ocean Studio) where a human can approve the action. _(execution: `<execution id>`)_ _This is structured guidance, not a crash — do not retry the identical call. Follow the remediation, or relay it to the user._ ``` Nothing reached the agent. `plungeai_execute_tool` and `plungeai_execute_agent` refuse a floor operation before a run exists, so no execution row is written; a floor operation that only appears at run time (a templated `operation` in a workflow task) is refused by the engine and leaves a `failed` execution with `outcome` `needs_approval`. There is no paused session behind either, so `plungeai_continue` cannot resume it (it answers "No conversation session for this execution (expired or not a conversational agent)"). The operation can only run from an interactive session where a human approves it: Ocean Studio. The same floor applies to any task in a workflow you run with `plungeai_execute_workflow`. This request is refused by design (not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_tool","arguments":{"user_request":"Email the report to the team","agent_id":"resend","operation":"send","params":{"from":"reports@example.com","to":"team@example.com","subject":"Weekly report","text":"…"}}}}' ``` **2. An agent stages the action and pauses.** Agents with their own approval step (the x402 broker, the concierge, bots) stop before spending and store a pending action. The run comes back as: ```markdown ⏸ NEEDS APPROVAL — `x402-broker` ⏸ AWAITING USER APPROVAL: <the staged action> (<its price>). Relay this to the user verbatim and ask them to decide — NEVER approve on your own. …the agent's own message so far (what it found, what it proposes)… **To proceed:** User approval required before the action runs. - Ask the user to approve, then call plungeai_continue {execution_id: "<id>", approve: true}. NEVER approve on your own. _(execution: `<id>`)_ _This is structured guidance, not a crash — do not retry the identical call. Follow the remediation, or relay it to the user._ ``` Here `plungeai_continue` is the way forward, and only after the user said yes ([Runs, results & follow-ups](/mcp/runs-results)). | | Engine floor refusal | Agent pause | |---|---|---| | Trigger | `operation`/`action` on the floor list | The agent staged a pending action | | Agent called? | No | Yes. It stopped before the action | | `remediation.actions` | none | `approve_via` → `plungeai_continue` | | Execution row | none when refused before the run; `failed`, outcome `needs_approval` when the engine floor fires at run time | `completed`, outcome `needs_approval` (the paused session is stored) | | How to proceed | Run it from Ocean Studio with a human approving | User says yes → `plungeai_continue {approve: true}` | **Card-level gates.** A card can mark more operations with `gated:<op>` tags (for example `x402-broker` → `gated:execute`). The contract shows them as `⚠️ GATED`. The MCP gateway checks card tags itself before the run: a typed call that names a `gated:` operation comes back `⏸ NEEDS APPROVAL`. A prompt-only call to the broker carries no operation, so it reaches the agent, which stops for approval before it pays. For the broker, the agent itself always stops for approval before paying (its card: "execute (pay-and-call in USDC — always stops for user approval, never auto-runs)"). **Payment and blockchain categories.** Agents in the `payment` and `blockchain` categories are not callable over MCP unattended, whatever the operation, exactly as on the One API's tools plane (https://api.plungeai.com/docs#6-tools-plane). `plungeai_execute_tool`, `plungeai_execute_agent` and every task of `plungeai_execute_workflow` come back `⏸ NEEDS APPROVAL` with nothing run. Today that is `crossmint-agent`, `x402-wallet-agent`, `circle-agent` and `circle-wallet-agent`, read operations included. Run them from Ocean Studio, where a human approves. Inside a [mission](/mcp/agents-missions), the loop's `call_agent` refuses gated operations and payment/blockchain agents unattended. ## 6.7 Connected accounts and your own keys Some agents act **as you**: your Gmail, your OneDrive, your wallet. They need a credential only you can provide. The requirement comes from one ladder (`resolveCredentialRequirements`, `credential-preflight.ts`): 1. `cred:<tier>:<provider>` tags on the card (authoritative, for example `x402-wallet-agent` → `cred:byok_api_key:x402-wallet`); 2. else a curated map of 40 agents (frozen; new agents declare credentials on their card); 3. else platform-managed: nothing for you to connect. | Tier | Meaning | Families | |---|---|---| | `platform` | The platform's own keys (Secret Store) | Most agents: search, documents, weather, SEC, LLMs, … | | `oauth_connector` (private connector) | Your account, connected by OAuth | Google Workspace (provider `google`: Gmail, Calendar, Drive, Docs, Sheets, Slides, Meet, Tasks, Forms, Chat, Contacts, Keep, Vault, Classroom, Admin, YouTube, Analytics, Ads, Search Console, Business Profile, Merchant Center, Workspace assistant, Docs export); Microsoft 365 (provider `azure`: Outlook mail, calendar and contacts, OneDrive, Teams, SharePoint, OneNote, Excel, Word, PowerPoint, To Do, Planner) | | `byok_api_key` (bring-your-own key) | A key or wallet credential you saved | `x402-wallet-agent` and `crossmint-agent` (`x402-wallet`), `circle-wallet-agent` (`circle-agent-wallet`), `circle-agent` (`circle`); `amazon-shopping-agent` (`amazon`, optional: shown, never blocking) | **Where you connect:** Ocean Studio, https://studio.plungeai.com → **Connectors**. Connect once. Every surface (MCP, Studio, the apps, the One API) then uses the same connection. For how accounts flow into calls, see https://api.plungeai.com/docs#7-connectors--connected-accounts. **When it is checked:** - `plungeai_get_tool_contract` shows the live status: "✅ connected as you@…", "⚠️ NOT connected", or "expired — reconnect" ([5.4](/mcp/discovery#54-the-invocation-contract--plungeai_get_tool_contract)). - `plungeai_execute_tool` checks **before** anything runs. A missing, expired or errored required credential returns `needs_connection` (if any unmet requirement is OAuth) or `needs_api_key` (if all are bring-your-own-key). The summary names the state (`not connected`, `expired — reconnect it`, `in error state — reconnect it`) and ends "Nothing was executed." The remediation says: "This needs the user, not you: relay the connect step to them, wait for confirmation, then retry the identical call." A `connect_provider` action includes up to eight OAuth scopes. - `plungeai_execute_agent` and `plungeai_execute_workflow` do not pre-flight. A missing credential surfaces when the run fails (401 → `needs_connection`), and the answer asks you to connect "the required service", because those paths do not pass the agent's declared provider to the classifier. The pre-flight fails open. If the status service does not answer, the call runs and the run-time classification ([6.4](#64-the-outcome-vocabulary)) is the backstop. Ask your client *"what does this agent need?"* to see the live status before first use. ## 6.8 Long runs: `mode: "async"` `mode: "async"` returns immediately with an `ok` outcome and the execution id: ```markdown Started `weather-agent · forecast` (async). Poll plungeai_get_workflow_status with execution_id `<id>`, then fetch output with plungeai_get_result. If the status reports ⏸ AWAITING USER APPROVAL or AWAITING USER, relay it to the user and continue with plungeai_continue. --- execution: `<id>` — use this id for plungeai_get_result / plungeai_followup on this run. ``` The run is held by its own Durable Object in ten-minute legs that resume from checkpoints ([Workflows](/mcp/workflows), [8.4](/mcp/workflows#84-sync-and-async)). There is no live feed. Failures, pauses and results are read with `plungeai_get_workflow_status` and `plungeai_get_result` ([Runs, results & follow-ups](/mcp/runs-results)). Use async when a run may outlast your client's tool-call limit: Claude Desktop cuts a single call at about 4 minutes. For shorter runs the sync call keeps the stream alive with a heartbeat every 15 seconds. ## 6.9 Payments Two agents handle money on the open x402 payment ecosystem. Both are active (live registry, 2026-09-24): | Agent | Category | What it does | Credential | Gates | |---|---|---|---|---| | `x402-wallet-agent` | `payment` | Your x402 wallet: `wallet_info` (address + USDC balance), `check_price` (the exact price of a **known** x402 URL), `fetch_paid` (pay a known URL within strict caps), `status` (poll a paid async job), `payment_requirements`. It never searches or chooses services: give it a URL | Your x402 Wallet connector (`x402-wallet`, bring-your-own) | Every operation over MCP (`payment` category); run it from Ocean Studio | | `x402-broker` | `shopping` | Prompt-driven, human-in-the-loop broker: discover services (Coinbase CDP Bazaar + Agentic.Market), inspect live prices at the origin, execute (pay-and-call in USDC), status, list wallets, ask you a question | Uses your connected x402 / Circle wallets | `execute` (card tag). Always stops for your approval | ### Try asking - *"Check my x402 wallet balance, then use the broker to find and buy access to a weather API."* The wallet call is refused over MCP (payment category); the broker handles discovery and asks for your approval before it spends anything. - *"Use the x402 broker to find StablePhone and place a call to [phone number]."* The broker discovers and pays for the calling service, asking your approval first, then places the call. Check on it afterwards with the broker (`status`). The wallet is a `payment` agent, so every call to it over MCP comes back `⏸ NEEDS APPROVAL` with nothing run, `wallet_info` included although it only reads. The refusal comes before the credential check, so it is the same with or without a connected wallet. Check your balance in Ocean Studio. The broker takes a sentence. This runs a model and can lead to a purchase after your approval (not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_tool","arguments":{"user_request":"Find an x402 weather API and tell me the price. Do not pay yet.","agent_id":"x402-broker","prompt":"Find an x402 weather API and inspect its exact live price at the origin. Do not pay anything."}}}' ``` A direct `fetch_paid` through `plungeai_execute_tool` is refused by the floor ([6.6](#66-the-trust-fence--needs-approval)). Unattended, it never spends. Pay through the broker's approval flow, or from Ocean Studio. The other money agents in the active catalog (live registry, 2026-09-24): `crossmint-agent` (`payment`, `x402-wallet` credential), `circle-agent` and `circle-wallet-agent` (`blockchain`, your Circle credentials) are refused over MCP whatever the operation, like the wallet. The shopping agents `amazon-shopping-agent` and `shopping-zinc` (`ecommerce-agents`) follow the floor list: any operation on it (`buy`, `purchase`, `pay`, `transfer`, `withdraw`, `shop`, …) comes back `⏸ NEEDS APPROVAL` with nothing run. Check an agent's contract first: its `⚠️ GATED` operations and its credential status are both listed there ([5.4](/mcp/discovery#54-the-invocation-contract--plungeai_get_tool_contract)). ### When a run pauses to ask you something Some agents stop instead of finishing: always before spending money, and sometimes to ask a question ("which wallet?", "what should the call say?"). Your client relays it and waits for you: 1. The run comes back with **⏸ AWAITING USER APPROVAL** (a staged action and its cost), as a `needs_approval` outcome, or **⏸ AWAITING USER** (a question), as a `needs_input` outcome. *Nothing has been executed yet.* 2. Your client asks you to decide. Reply naturally: **"yes"** / **"approve"** to proceed, **"no"** or any other answer to change course. The client is instructed never to approve on its own. 3. On approve, the client calls `plungeai_continue {execution_id, approve: true}` and the held action runs exactly once. A stale or repeated approve is harmless: the agent answers that nothing is awaiting approval, and it can never pay twice. To deny or change course, the client passes your words as `message`. Background (`async`) runs surface pending approvals too. `plungeai_get_workflow_status` returns them as `structuredContent.continuation`, and `plungeai_get_result` appends the ⏸ block, so a paused background run is never invisible ([Runs, results & follow-ups](/mcp/runs-results)). # Agents & missions Source: https://www.plungeai.com/docs/mcp/agents-missions Source: `orchestration/mcp-gateway/tools.ts` (`ExecuteAgentSchema`, `executeAgent`, `RunMissionSchema`, `plungeaiRunMission`, `fenceRefusal`, `renderRunResult`), `orchestration/mcp-gateway/registry-cards.ts` (`classifyCardAvailability`, `lowerToEngineTask`), `orchestration/mcp-gateway/execution.ts`, `orchestration/mcp-gateway/execution-do.ts`, `orchestration/mcp-gateway/leg-state.ts`, `orchestration/mcp-gateway/server.ts`, `core/core-base/provider-ids.ts`, `core/core-base/rate-card-client.ts`, `agents/agents/one-agent/call-config.ts` + `call-capabilities.ts` + `one-agent.ts` + `tools.ts` + `runtime.ts`, `orchestration/cnl-engine/executors.ts` (`executeHarnessTask`); live registry reads 2026-09-24 and 2026-10-04 (`one-agent`). Two tools run "one thing" without a workflow. `plungeai_execute_agent` runs one **prompt-driven** agent once, and you choose the model. `plungeai_run_mission` runs a bounded autonomous **mission**: a tool loop with a goal, a tool fence, an iteration cap and long-term memory. Structured tool-agents that take typed fields go through [`plungeai_execute_tool`](/mcp/tools) instead. | You want | Tool | Runs as | Default mode | |---|---|---|---| | One answer from a model or persona | `plungeai_execute_agent` | One `agent:` task | sync only | | One operation of a structured agent | `plungeai_execute_tool` ([Tools](/mcp/tools)) | One `agent:` task with typed fields | sync (`mode: "async"` optional) | | A goal worked on over several steps with tools | `plungeai_run_mission` | One `type: harness` task | **async** (`mode: "sync"` optional) | Base URL `https://mcp.plungeai.com/v1`. `https://api.plungeai.com/v1/mcp` is the same server. The One API equivalents are https://api.plungeai.com/docs#9-agents-plane and the `type: harness` task (https://api.plungeai.com/docs#107-the-type-harness-task). Always pass `user_request`. Parameter types and limits are in the [Tool reference](/mcp/tool-reference). ## 7.1 Run one prompt-driven agent — `plungeai_execute_agent` The agent is `one-agent`, the platform's agent runtime. Here it runs in call mode: one model answer, plain or shaped by a `persona`. It is an active registry agent (live 2026-10-04). The legacy ids `llm-agent` and `skill-agent` still answer until the switch-over, but new calls name `one-agent`. A pre-built agent card (tag `runtime:universal`) is also accepted: it is a stored capability pack, not a deployed Worker, so it runs as a harness task on the universal runtime (`lowerToEngineTask`). | Argument | Meaning | |---|---| | `agent` | Registry agent id (required) | | `prompt` | What to ask (required) | | `model`, `provider` | Model choice, checked before the run ([7.3](#73-choosing-a-model)) | | `max_tokens` (or `maxTokens`), `temperature`, `top_p`, `reasoning_effort` (`low`·`medium`·`high`·`xhigh`), `thinking_level` | Generation knobs, passed to the task as given. When both token spellings are sent, `maxTokens` wins | | `persona` | A persona (digital twin) id; `one-agent` answers in that voice. Find ids with `plungeai_list_agents` `kind: "personas"` | | `session_id` | Start or continue a conversational agent ([7.4](#74-conversations-and-personas)) | | `streaming` | Streaming is forced on by default. `false` opts this call out | | `format` | `markdown` (default) or `json` | *Try asking: "Ask one-agent in one sentence what a Cloudflare Durable Object is."* It runs a single agent once, with no workflow needed. This runs a model (it costs credits and writes an execution; not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_agent","arguments":{"user_request":"Ask one-agent in one sentence what a Cloudflare Durable Object is.","agent":"one-agent","prompt":"In one sentence: what is a Cloudflare Durable Object?","max_tokens":120}}}' ``` On success the result `text` is the agent's answer plus the execution footer (`---` / ``execution: `<id>` — use this id for plungeai_get_result / plungeai_followup on this run.``). `structuredContent` is the outcome envelope ([6.4](/mcp/tools#64-the-outcome-vocabulary)): `{"status":"ok","summary":"Agent completed.","result":"…","execution_id":"…"}`. The run shows in your history and in Studio's sidebar under the agent's id, titled with your prompt. ## 7.2 Before it runs: the agent fence The card is checked before any execution exists (`classifyCardAvailability`). A denial is not a run, so no failed row lands in your history: | Case | Outcome | Summary | |---|---|---| | No card with that id | `unavailable` | "No such agent: `<id>` — that id does not exist in the registry (agent names cannot be guessed or carried over from other platforms). Discover callable agents with plungeai_list_agents — describe the capability in natural language (its `search` is semantic) and retry with an id from the results." | | Card exists, not `status:active` | `unavailable` | "Not permitted: `<id>` is not `status:active`, and only active agents are callable over MCP. Discover callable agents with plungeai_list_agents …" | Both answers include up to three live alternatives (`use_alternative`). If the registry does not answer at all, the call proceeds (fail-open), and a truly missing agent then fails in the engine as "registered but not deployed" (`unavailable`). > **The legacy loop id `harness-agent` is not a registry id.** It has no registry card (the registry answers 404, live-verified 2026-09-24), so `plungeai_execute_agent` refuses it as "No such agent". For a loop, use `plungeai_run_mission` ([7.6](#76-run-a-bounded-mission--plungeai_run_mission)), which runs on `one-agent`. ## 7.3 Choosing a model *Try asking: "Ask one-agent, using claude-haiku-4-5, to summarize this paragraph."* You can pick the model for a single-agent run. The model name is checked against the live catalog **before** anything runs. A name that doesn't exist is refused at once with suggestions; it never hangs. How the check works (`executeAgent`): - The catalog is the platform rate card's model catalog (`model_catalog`, read from the rate-card snapshot). - A dated variant matches its catalog row by prefix: `claude-sonnet-4-5-20250929` matches `claude-sonnet-4-5`. - Pass the model id as the catalog spells it, normally the bare id (`claude-haiku-4-5`, `gemini-3.1-flash-lite`). A registry model-card id such as `anthropic:claude-haiku-4-5-20251001` is not a catalog id and is refused. - A known model also sets the task's **provider** from its catalog row, so a `claude-*` model reaches Anthropic even when the agent's default provider is another one. - An explicit `provider` wins over the catalog. It is lower-cased and mapped to the runtime id: `google` → `gemini`, `google-vertex` → `vertex`, `x-ai` → `xai` (`toRuntimeProvider`). - If the catalog itself cannot be read, the check is skipped and the run proceeds (fail-open). - With no `model`, the agent uses its default. For `one-agent` that is `gemini-3.1-flash-lite` on `gemini` (`agents/agents/one-agent/call-config.ts`). An unknown model returns `needs_input` with nothing executed: ```markdown 📝 NEEDS INPUT — `one-agent` Unknown model `claude-haiku-9` — it is not in the platform model catalog, and dispatching it would only fail after a long streaming timeout. Nothing was executed. Omit `model` to use the agent's default, or pick a catalog model: `<up to five catalog models whose id contains "claude">` (<provider>), …. _This is structured guidance, not a crash — do not retry the identical call. Follow the remediation, or relay it to the user._ ``` The suggestions are catalog models whose id contains the first token of your name (the part before the first `-` or `.`). If none match, the answer suggests `claude-sonnet-4-5` or `gemini-3.1-flash-lite` as examples. *"What models are available?"* The client answers with `plungeai_list_agents` `kind: "models"` ([Discovery](/mcp/discovery)). Model cards are named `provider:model-id`. Pass only the part after the colon as `model`. The registry's model cards and the rate card's catalog are separate lists, so a model shown there can still be refused here; the refusal then lists catalog models to pick from. This runs a model (it costs credits; not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_agent","arguments":{"user_request":"Ask one-agent, using claude-haiku-4-5, to summarize this paragraph.","agent":"one-agent","model":"claude-haiku-4-5","prompt":"Summarize in two sentences: <paragraph>","temperature":0.2}}}' ``` ## 7.4 Conversations and personas - **`session_id`** is passed to the agent task. A conversational agent keeps its transcript under that id: send the same `session_id` on the next call to continue. Every run's execution id also doubles as its conversation session id. That is how `plungeai_followup` and `plungeai_continue` pick a finished or paused run back up ([Runs, results & follow-ups](/mcp/runs-results)). - **`persona`** selects a digital twin, one voice per call (`one-agent` call mode, `call-capabilities.ts`). Persona ids come from `plungeai_list_agents` `kind: "personas"`. - **Pauses.** If the agent stops to ask a question or to get approval, the answer is `needs_input` (`⏸ AWAITING USER: …`) or `needs_approval` (`⏸ AWAITING USER APPROVAL: …`) with a `plungeai_continue` action ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)). - For a persistent chat with the platform assistant, use `plungeai_chat` ([Chat, memory & skills](/mcp/chat-memory-skills)). ## 7.5 Outcomes and long answers `plungeai_execute_agent` has **no async mode**. The call stays open until the agent answers. While it waits, the server streams progress: token frames (`✍️ Writing (N chars) — …`), the engine's heartbeat (`⚙️ one-agent still working — 45s elapsed…`), and a keepalive every 15 seconds (`⏳ Working — Ns elapsed`). Whether your client shows them is up to the client ([4.6](/mcp/protocol#46-toolscall-over-sse-progress-and-keepalive)). Claude Desktop cuts a single tool call at about 4 minutes. For anything that long, use `plungeai_run_mission` (async by default), or put the agent in a one-task workflow and run it with `plungeai_execute_workflow` `mode: "async"` ([Workflows](/mcp/workflows)). Failures are classified into the same outcome vocabulary as tools ([6.4](/mcp/tools#64-the-outcome-vocabulary)): a run-time 401 becomes `needs_connection`, a timeout becomes `error` with a retry-async hint, and so on. The status is stamped on the execution's `outcome` column. `plungeai_execute_agent` does no credential pre-flight. If an agent needs your connected account, check it first with `plungeai_get_tool_contract` ([6.7](/mcp/tools#67-connected-accounts-and-your-own-keys)). ## 7.6 Run a bounded mission — `plungeai_run_mission` A **mission** bounds what an agent may do for one goal: a purpose, a fenced tool set, an iteration cap, optional success criteria, a persona and skills. `plungeai_run_mission` wraps your arguments into a one-task CNL workflow and runs it on `one-agent` in loop mode, a real ReAct tool loop. This is the task it builds (`plungeaiRunMission`): ```yaml workflow: name: mcp-mission tasks: - type: harness id: mission agent: one-agent # or the agent you pass goal: <goal> mission: mission: <mission, or the default purpose below> allowed_tools: [web_search, web_fetch, task_complete] # unless you pass allowed_tools max_iterations: 8 # unless you pass max_iterations success_criteria: [...] # only if you pass them persona: <id> # only if you pass it skills: [...] # only if you pass them ``` | Argument | Default | Meaning | |---|---|---| | `goal` | required | The per-run task, in plain language | | `agent` | `one-agent` | The loop runtime that runs the mission. The gateway always sends it: a `type: harness` task with no `agent:` line still falls to the legacy `harness-agent` in the engine until the switch-over. A runtime you name passes the same active-only fence as `plungeai_execute_agent` | | `mission` | "You are a capable autonomous agent. Achieve the goal thoroughly and cite sources." | The standing purpose, which becomes the system-prompt frame | | `allowed_tools` | `[web_search, web_fetch, task_complete]` | The tool fence. Only these reach the model ([7.7](#77-mission-bounds)) | | `max_iterations` | `8` (max 50) | The loop's turn cap | | `success_criteria` | none | Statements the agent checks itself against. They are added to the system prompt as "the task is done only when all are met" | | `persona` | none | One persona id, one voice for the run | | `skills` | none | Skill ids (up to 20), injected into the run | | `mode` | **`async`** | `sync` waits for the result, with live progress | The mission shows in your history as `mcp-mission`, titled with the goal. Every run also carries `user_context` (your time zone and approximate place, taken from the request's network location), which the agent uses for local time and as the default time zone for a schedule. *Try asking: "Ask One Agent a quick question: what's notable about Sun Valley, Idaho?"* That is a bounded, one-shot mission, fast and reliable. Keep goals like this short and specific. An open-ended "research everything about X" goal can run long enough that you check on it later ([Runs, results & follow-ups](/mcp/runs-results)). This runs a model loop with web tools (it costs credits and writes an execution; not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_run_mission","arguments":{"user_request":"Ask One Agent a quick question: what is notable about Sun Valley, Idaho?","goal":"In five bullet points: what is notable about Sun Valley, Idaho? Cite a source URL for each.","max_iterations":6,"success_criteria":["five bullets","every bullet has a source URL"]}}}' ``` The async answer comes back at once. It is an `ok` outcome carrying the execution id: ```markdown Started mission `<execution id>` (async). Poll plungeai_get_workflow_status with this id, then fetch output with plungeai_get_result once completed. The agent recalls and writes long-term memory automatically. If the status reports ⏸ AWAITING USER APPROVAL or AWAITING USER, relay it to the user and continue with plungeai_continue. --- execution: `<execution id>` — use this id for plungeai_get_result / plungeai_followup on this run. ``` Then poll `plungeai_get_workflow_status` and read `plungeai_get_result` ([Runs, results & follow-ups](/mcp/runs-results)). With `mode: "sync"` the call waits and returns `Mission completed.` with the result, or a classified outcome (the mission's agent, `one-agent` unless you passed `agent`). ## 7.7 Mission bounds **The tool fence.** Only the tools in `allowed_tools` reach the model. The one-agent catalog has 27 tools (`ALL_TOOLS`, `agents/agents/one-agent/tools.ts`): `read_file`, `write_file`, `edit_file`, `list_files`, `search_files`, `delete_file`, `web_search`, `web_fetch`, `load_skill`, `memory`, `knowledge`, `skill_manage`, `recall`, `recall_history`, `delegate`, `schedule`, `message_bot`, `browser`, `local_agent`, `registry_search`, `registry_lookup`, `call_agent`, `invoke_workflow`, `run_python`, `ask_user`, `platform_action`, `task_complete`. The concierge spellings `find_agent`, `agent_card` and `run_agent` are accepted as aliases of `registry_search`, `registry_lookup` and `call_agent`. Include `task_complete` so the agent can finish. **The iteration cap.** `max_iterations` (the legacy alias of the harness `max_turns`) stops the loop. An async mission that hits the cap before `task_complete` is reported **`incomplete`** by `plungeai_get_workflow_status`, not `completed`, and its partial output is still stored and retrievable ([Runs, results & follow-ups](/mcp/runs-results)). In `mode: "sync"` the answer reads `Mission completed.` even then: the sync path stamps the execution's outcome `ok` after the run, which replaces the `incomplete` mark (`renderRunResult` in `tools.ts`). Use async when you need to tell a finished mission from one that ran out of turns. **Unattended posture.** An inline mission runs on `one-agent` unless `agent` names another runtime; one-agent's host profile is unattended (`ONE` in `one-agent.ts`). If you allow `call_agent`, the loop's agent fence refuses outward, irreversible operations (the floor list in [6.6](/mcp/tools#66-the-trust-fence--needs-approval)) and agents in the `payment` and `blockchain` categories, unless a mission's `allowed_agents` names them (`core/core-base/call-agent.ts`). `plungeai_run_mission` has no `allowed_agents` argument, so a mission started here cannot call payment agents. **Questions.** With `ask_user` in `allowed_tools`, the mission can pause with a question. `plungeai_get_workflow_status` then reports it as `continuation`, and you answer with `plungeai_continue` ([Runs, results & follow-ups](/mcp/runs-results)). **More mission fields.** Experts, backgrounds, plugins, MCP servers, effort presets, spend caps and permissions are mission fields in CNL, but `plungeai_run_mission` does not expose them. For those, write a `type: harness` task yourself and run it with `plungeai_execute_workflow` ([8.10](/mcp/workflows#810-cnl-essentials-for-ad-hoc-yaml); https://api.plungeai.com/docs#14-skills-plugins--capability-injection). ## 7.8 Memory A mission runs with your long-term memory, which is scoped to you: - **Recall at the start.** A top-level mission (not a fan-out member, not a delegated sub-mission) gets a frozen snapshot of your memory prepended to its system prompt. It is built once per run and is best-effort: memory being down never blocks the run (`runtime.ts`, `executors.ts`). - **Write-back at the end.** After the run, a background review curates durable facts from it into your memory. No `memory` tool is needed for that. - **Explicit writes mid-run** need `memory` in `allowed_tools`. To read or write memory yourself (*"What do you remember about Circle's acquisitions?"*, *"Remember that our fiscal year ends in June."*), use `plungeai_memory` ([Chat, memory & skills](/mcp/chat-memory-skills)). ## 7.9 Async missions: the Ten-Minute Relay Async missions (and every other `mode: "async"` run) are held by a per-run Durable Object, not by your tool call (`execution-do.ts`, `leg-state.ts`): - The run executes in **legs**. A harness leg hands over at the first turn boundary after **10 minutes**, and the next leg resumes from the stored checkpoints. - A leg that is killed (deploy, host restart, the 15-minute alarm cap) is re-entered by a watchdog alarm set **12 minutes** after the leg started, and repeats at most the turn that was in flight. - **Three** killed legs in a row with no checkpoint fail the run: "Run was killed 3 times in a row without yielding a checkpoint. … Split the work or shorten the step, then re-run to retry." - If the dispatch itself fails, the execution is marked failed with `Async dispatch failed: …`. Because the Durable Object holds the run, it survives your client disconnecting. There is no live progress feed on async runs. Poll `plungeai_get_workflow_status`, which also self-heals a row whose run finished but was never closed ([Runs, results & follow-ups](/mcp/runs-results)). # Workflows Source: https://www.plungeai.com/docs/mcp/workflows Source: `orchestration/mcp-gateway/tools.ts` (`ExecuteWorkflowSchema`, `executeWorkflow`, `ListWorkflowsSchema`, `listWorkflows`, `WorkflowSchema`, `plungeaiWorkflow`, `BuildWorkflowSchema`), `orchestration/mcp-gateway/workflow-store.ts`, `orchestration/mcp-gateway/chat.ts` (`buildWorkflow`), `orchestration/mcp-gateway/extras.ts` (`plungeaiTemplates`), `orchestration/mcp-gateway/execution.ts` (`runExecution`, `executeViaStream`), `orchestration/mcp-gateway/execution-do.ts`, `orchestration/mcp-gateway/leg-state.ts`, `orchestration/mcp-gateway/tool-outcome.ts`, `orchestration/mcp-gateway/server.ts`, `orchestration/cnl-engine/cnl-engine.ts` (`loadWorkflow`, request `input` / `inputs`), `agents/agents/one-agent/call-config.ts`, `agents/agents/one-agent/call-mode.ts`, `core/core-base/provider-ids.ts`, `apps/ocean-skills/skills/plungeai-workflows/references/cnl-spec.md`. **Your workflows** are saved CNL workflows. CNL is the platform's YAML workflow language. Users call their workflows "my agents", and most day-to-day work is here: find a workflow, run it, inspect it. There is no separate "MCP workspace". A workflow you create or run here is the same workflow you see in Studio and the apps, and every change syncs live. | Job | Tool | |---|---| | Find your workflows and folders | `plungeai_list_workflows` ([8.1](#81-find-your-workflows--plungeai_list_workflows)) | | Run a saved workflow, or ad-hoc YAML | `plungeai_execute_workflow` ([8.2](#82-run-a-workflow--plungeai_execute_workflow)) | | Create, read, edit, delete, version | `plungeai_workflow` ([8.6](#86-manage-saved-workflows--plungeai_workflow)) | | Generate or refine one from plain language | `plungeai_build_workflow` ([8.8](#88-generate-a-workflow--plungeai_build_workflow)) | | Copy a ready-made template into your account | `plungeai_templates` `use` ([8.9](#89-start-from-a-template--plungeai_templates-use)) | Base URL `https://mcp.plungeai.com/v1`. `https://api.plungeai.com/v1/mcp` is the same server; the REST twin is https://api.plungeai.com/docs#10-workflows-plane. Always pass `user_request`: for "show me all my agents in the finance folder", the arguments alone are just `{folder: "Finance"}` and lose the intent. Parameter types and limits are in the [Tool reference](/mcp/tool-reference). ## 8.1 Find your workflows — `plungeai_list_workflows` | You send | What comes back | |---|---| | nothing | `# Your Workflows (N most recent)`, then `**Folders:** Finance (15) · Personal (23) · …` (only folders that hold something, in your sidebar order), then a table of your 50 most recently updated workflows | | `folder` | Workflows whose folder name **contains** this text (case-insensitive) | | `search` | Workflows whose **name** contains this text (case-insensitive). Descriptions are not searched | | `kind` | Only that kind: `workflow`, `agent`, `bot` or `campaign` (exact) | | `folder` + `search` + `kind` | All given must match | | `limit` | 1 to 100 (default 50) | | `format: "json"` | `{"workflows":[{"id","name","description","kind","folder"}]}` | The table columns are Name, Kind (`workflow`, `agent` or `bot`), Folder, ID, Description, newest first. The ID is what `plungeai_execute_workflow` and `plungeai_workflow` take. With no match you get "No workflows matched. List without filters to see your folders." An empty account answers "No workflows found. Create one with plungeai_workflow or plungeai_build_workflow." The tool is read-only. ### Try asking - *"Show me all the workflows in my Finance folder."* - *"What folders do I have and how many workflows in each?"* For example: Finance 15 · Personal 23 · Technology 11 · Business Strategy 14 · Blockchain 8. - *"Find my workflow about due diligence."* This searches your workflow **names**, so a workflow called "DD Memo" is not found by "due diligence". List the folder instead if the name is different. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_list_workflows","arguments":{"user_request":"What folders do I have and how many workflows in each?"}}}' ``` > **"Category" and "folder" are different things.** Your *folders* (Finance, Personal, …) hold **your workflows**. *Categories* are the registry's agent taxonomy ([Discovery](/mcp/discovery)), where the finance one is called `financials`. ## 8.2 Run a workflow — `plungeai_execute_workflow` | Argument | Meaning | |---|---| | `workflow_id` | A saved workflow you own (the ID from 8.1) | | `workflow_yaml` | Ad-hoc CNL, up to 256 KiB ([8.3](#83-ad-hoc-cnl-yaml)) | | `input` | The primary input, available as `{input}` | | `inputs` | Named inputs `{key: value}`, available as `{key}` | | `mode` | `sync` (default) or `async` ([8.4](#84-sync-and-async)) | | `format` | `markdown` (default) or `json` | Send `workflow_id` **or** `workflow_yaml`. With neither you get `needs_input` ("Provide either workflow_yaml (ad-hoc CNL) or workflow_id (a saved workflow)."). If you send both, `workflow_id` is used and the YAML is **ignored**. **Saved workflows.** The id must be a workflow on *your* account. An unknown id, or one owned by someone else, returns `needs_input` ("No saved workflow with id `<id>` on this account.") with a pointer to `plungeai_list_workflows`, and no run. The run is named after the saved workflow. Its title in Studio's sidebar is your `input` (or the `inputs` values, or your `user_request`), the same as a run started in Studio. ### Try asking - *"Run the Acquisition Scanner with Adobe as the input."* This runs a saved workflow. Most finish in seconds, because the engine fans out over agents in parallel. - *"Show me my workflows in the Finance folder, then run the Acquisition Scanner with Tesla as the input."* One request, two steps. Your client lists the folder to find the workflow you meant, then runs it with your input. - *"Run my Company Snapshot Research Brief with Apple as the input."* The same pattern: swap in whichever workflow and company you mean. This runs your workflow's agents (it costs credits and writes an execution; not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_workflow","arguments":{"user_request":"Run the Acquisition Scanner with Adobe as the input.","workflow_id":"<workflow id from plungeai_list_workflows>","input":"Adobe"}}}' ``` A saved workflow's agents are **not** re-checked by the gateway at run time. Only ad-hoc YAML goes through the active-only fence (8.3). If a saved workflow names an agent that is no longer deployed, the run fails in the engine and comes back `unavailable` ("registered but not deployed"). ## 8.3 Ad-hoc CNL YAML `workflow_yaml` runs CNL without saving it. The `workflow:` wrapper is optional: bare top-level `name` + `tasks` is accepted and wrapped for you (Form B in the CNL spec). This example needs `jq` to build the JSON. It runs three agents (it costs credits; not run by the docs verifier): ```bash YAML=$(cat <<'CNL' name: Quick brief tasks: - type: parallel id: research subtasks: - id: web agent: brave-agent prompt: "{input} — latest news" - id: deep agent: tavily-agent prompt: "{input} — cited sources from the last 12 months" - id: brief agent: one-agent prompt: | Write a five-bullet brief on {input}. Brave: {data:web} Tavily: {data:deep} CNL ) jq -n --arg yaml "$YAML" '{jsonrpc:"2.0",id:1,method:"tools/call",params:{name:"plungeai_execute_workflow",arguments:{user_request:"Quick brief on Cloudflare Workers",workflow_yaml:$yaml,input:"Cloudflare Workers"}}}' | curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d @- ``` What the gateway does before the engine sees it (`executeWorkflow`): 1. **Parses** the YAML. If it cannot be parsed, you get `needs_input` ("Your workflow_yaml could not be parsed. \<parser message\>"), and the message points only at *your* text. Valid YAML that is a bare scalar (a string, a number) or empty returns `needs_input` ("workflow_yaml must be a CNL mapping with a `workflow:` (or top-level `name`/`tasks`), not a bare value."). Nothing runs in either case. A top-level **list** is not caught here: it is wrapped and dispatched without the agent fence below, fails in the engine, and leaves a failed execution in your history. Always send a mapping with `workflow:` or top-level `name`/`tasks`. 2. **Fences the agents.** The `agent:` ids of your tasks are checked against the registry, including inside `subtasks`, batch `tasks` and `executors`, plus a harness task's `mission_ref` (`collectAgentIds`). Unknown or not-`status:active` ids return `unavailable`, naming each offender ("No such agent: …", "Not permitted: … not `status:active`"). Nothing runs. Agents named elsewhere (a `dynamic` generator, debaters, judges, validators) are resolved by the engine when the run starts. 3. **Wraps and runs.** Your `input` and `inputs` travel with the workflow and fill `{input}` and `{key}`. The run is named `<YAML name> · <first three agents>` (`+N` for more). After the run, the name is replaced by the agents that actually ran. Every run also carries `user_context` (your time zone and approximate place, taken from the request's network location), which the agent uses for local time and as the default time zone for a schedule. The gateway does not run the CNL schema validator on ad-hoc YAML, and the engine records schema findings without blocking the run (its validation mode is record-only; `cnl-engine.ts`). A malformed document is dispatched and fails however its tasks fail, and that leaves a failed execution in your history. To check a document first, save it with `plungeai_workflow` `create`: that call runs the validator (`yamlToCNL`) and rejects errors such as `Workflow name is required and must be a string` or `Type 'harness' requires a 'goal' string` before anything is stored. Or let `plungeai_build_workflow` write the YAML. ### Writing YAML by hand - **Quote your colons.** `prompt: DD: memo` breaks YAML. Write `prompt: "DD: memo"`, or put long text in a block scalar (`prompt: |`). Never hard-wrap a value across lines. An `invalid yaml` failure comes back with exactly this tip. - **Structured tool-agents take their card's fields as task fields**, next to `agent:`: ```yaml - id: wx agent: weather-agent operation: forecast city: "Sun Valley,ID,US" units: imperial ``` Watch for card fields that share a name with a CNL task field. `location` is one: on a task it means `cloud` or `local` (where the task runs). A weather task with `location: "Sun Valley,ID,US"` runs as ad-hoc YAML, because the field passes through to the agent, but the validator used by `plungeai_workflow` and `plungeai_build_workflow` rejects it (`Invalid location: Sun Valley,ID,US. Must be 'cloud' or 'local'`). In a saved workflow, use the weather card's alias `city` (as above) or `lat`/`lon`. - **On a `one-agent` task, prefer `provider: gemini` or omit `provider`.** Omitting it gives the default (`gemini`, model `gemini-3.1-flash-lite`, `agents/agents/one-agent/call-config.ts`). `google` is accepted and mapped to `gemini` by the provider resolver (`core/core-base/provider-ids.ts`), but one-agent then does not apply its default model (it does so only for an omitted or literal `gemini` provider), so pass `model` explicitly if you write `google`. Any other unknown provider is refused (`Unsupported provider: <x>`, [Troubleshooting](/mcp/troubleshooting)). - **Take agent ids from a live search**, never from memory: it is `brave-agent`, not `brave-search` ([Discovery](/mcp/discovery)). - **Outward operations are fenced here too.** A task whose `operation` or `action` is `send`, `pay`, `buy`, `transfer`, … is refused by the engine as `needs_approval` ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)). Nothing runs, and `plungeai_continue` cannot resume it: such a task runs only from Ocean Studio. - **There is no `schedule:` field.** Schedule a workflow with `plungeai_schedule` ([Scheduling](/mcp/scheduling)). Easier still: describe the goal and let *"build me a workflow that…"* write the YAML for you ([8.8](#88-generate-a-workflow--plungeai_build_workflow)). ## 8.4 Sync and async **Sync** (the default) runs the workflow and streams live status while it works. The lines come from the engine's own task events, the same ones Studio's live view renders (`executeViaStream`): ``` ⚙️ Agents working — 5/18 agents complete · ✓ brave-agent · 13 active (tavily-agent, exa-agent, serper-agent…) ✍️ Generating answer — 16/18 agents complete · 2 active (one-agent) ✍️ Writing (3516 chars) — …the answer as it is produced ⚙️ one-agent still working — 45s elapsed… ✅ Complete — 18 agents in 24.6s ``` A keepalive (`⏳ Working — Ns elapsed`) is sent every 15 seconds so the stream never goes idle. The feed goes out on `notifications/progress` when your client sent a progress token, otherwise on `notifications/message`. **Whether it is displayed is up to your client**; the work and the result are the same either way ([4.6](/mcp/protocol#46-toolscall-over-sse-progress-and-keepalive)). A quiet client is not a stalled run: the run is visible in Studio's sidebar the whole time. **Async** (`mode: "async"`) returns at once with the execution id. The tool description suggests it for runs longer than about 3 minutes, and it is the way around Claude Desktop's roughly 4-minute cap on a single call: ```markdown Started execution `<execution id>` (async). Poll plungeai_get_workflow_status with this id, then fetch output with plungeai_get_result once completed. If the status reports ⏸ AWAITING USER APPROVAL or AWAITING USER, relay it to the user and continue with plungeai_continue. --- execution: `<execution id>` — use this id for plungeai_get_result / plungeai_followup on this run. ``` An async run has no live feed. It is held by its own Durable Object in resumable ten-minute legs, so it survives your client disconnecting ([7.9](/mcp/agents-missions#79-async-missions-the-ten-minute-relay)). Poll `plungeai_get_workflow_status`, then fetch `plungeai_get_result` ([Runs, results & follow-ups](/mcp/runs-results)). A running execution that stops heartbeating for 16 minutes is marked failed, with `Interrupted (run did not complete)`. Rerun it, in the foreground if it is short. ## 8.5 What a run returns | Outcome | When | |---|---| | `ok` | The final task's output, then the execution footer (`summary`: "Workflow completed."). If the final task stored nothing, the stored outputs of the individual tasks are joined under `## <task id>` headings instead. An `async` start is also `ok` | | `needs_approval` / `needs_input` | A task paused for approval or a question (`⏸ AWAITING USER APPROVAL: …` / `⏸ AWAITING USER: …`). Continue with `plungeai_continue` after the user decides | | `needs_approval` (no actions) | A task's `operation`/`action` is on the engine floor list. Nothing ran, and it cannot be continued over MCP ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)) | | `needs_input` | No workflow given, unknown saved id, unparseable YAML, or an agent rejected its inputs | | `unavailable` | Ad-hoc YAML names an unknown or inactive agent, or an agent is not deployed | | `needs_connection` / `needs_api_key` | A task hit a missing connected account or key at run time. Workflows have no pre-flight, so the answer names "the required service" ([6.7](/mcp/tools#67-connected-accounts-and-your-own-keys)) | | `error` | A genuine failure with the real reason. A timeout suggests `mode: "async"` | Every outcome follows the vocabulary in [6.4](/mcp/tools#64-the-outcome-vocabulary). Two failure shapes are specific to runs. If the engine reports success but nothing was stored, the run is failed: "Engine reported success but no result was stored (final_task: …). The agent likely failed in-band without storing output — check its own logs." If the final answer *starts* with a refusal marker (`# ❌`, `Error: `), the run is failed with that text, never shown as a success. A run whose final harness task ran out of turns is stored as `incomplete` and reported that way by `plungeai_get_workflow_status`, but only for **async** runs. A sync run's answer is `ok`, and the sync path stamps the outcome `ok` afterwards, replacing the `incomplete` mark ([7.7](/mcp/agents-missions#77-mission-bounds)). **Every run is visible everywhere.** It appears in Studio's results sidebar and the ops dashboard. The title is your question, and the subtitle is the workflow plus the agents that actually ran, the same as a run started in Studio. The result is final Markdown (tables, headings, links). Ask for it "in full" if your client tends to summarize. `plungeai_get_result` adds a **"Steps in this run"** index for multi-task runs, so any step's full output is one ask away ([Runs, results & follow-ups](/mcp/runs-results)). ## 8.6 Manage saved workflows — `plungeai_workflow` Direct edits to your saved workflows. The store is byte-compatible with Studio's (D1 `yaml_workflows` + KV `workflow:{user}:{id}`), and every write is ownership-checked. Create, update and delete are broadcast and sync live to Studio, iMini and the other apps. `save_version` and `restore_version` are not broadcast, so reload an open Studio editor after a restore (saving from a stale editor would overwrite the restored YAML). | `action` | Needs | Optional | Does | Answers | |---|---|---|---|---| | `create` | `name`, `yaml` | `description`, `folder`, `kind` | Validates the CNL, saves, files into the folder | "Created workflow **\<name\>** (`<id>`). Filed under **\<folder\>**. Synced to all apps." plus any validation warnings | | `get` | `workflow_id` | — | Reads the saved definition | `# <name>`, the description, and the YAML in a code block | | `update` | `workflow_id` | `name`, `description`, `yaml`, `folder`, `kind` | Changes only what you send. New YAML is validated first. Moving `kind` away from `bot` or `campaign` runs the same clean-up as `delete` for the old kind (below) | "Updated workflow `<id>`. Synced to all apps." plus " Moved to folder **\<folder\>**." / " Kind set to **\<kind\>**." | | `delete` | `workflow_id` | — | Deletes the workflow (the D1 row and its live content). A bot also loses its memory namespace `<you>:bot:<id>` and its scheduler jobs; a campaign loses its scheduler jobs and its ledger is tombstoned | "Deleted workflow `<id>`. Synced to all apps." | | `save_version` | `workflow_id` | `version_description` | Snapshots the current YAML as the next version ([8.7](#87-versions)) | "Saved version V\<n\> (`<version id>`)." | | `list_versions` | `workflow_id` | — | Lists the snapshots, newest first | A Version / ID / When / Note table, or "No versions yet. Use action "save_version"." | | `get_version` | `workflow_id`, `version_id` | — | Reads one snapshot | `# V<n>` and its YAML | | `restore_version` | `workflow_id`, `version_id` | — | Makes that snapshot live again | "Restored — new version V\<n\>." | | `permissions_get` | `workflow_id` (a saved bot) | — | Reads its permission classes, locks and spend caps, and `effective`: the preset each of the four classes (`send`, `write`, `pay`, `delete`) runs under, the stored one or else `allow` | classes, effective, locks, `budget_usd_run`, `budget_usd_month`, and the same as JSON | | `permissions_set` | `workflow_id` | `classes`, `locks`, `budget_usd_run`, `budget_usd_month` | Each field present replaces the stored one; `locks` only for a team admin over the workflow (otherwise ``Error: … (lock_admin_required)`` and nothing is written) | "Updated." and the new policy | | `files` | `workflow_id` | — | Lists the bot's persistent workspace | a Path / Size / Type / Updated table | | `file` | `workflow_id`, `path` | — | Reads one workspace file, at most 5 MB | the bytes as a base64 block (larger: use the One API download route) | | `inbox` | `workflow_id` | — | Reads the bot-to-bot messages queued for its next run | a From / When / Message table | | `bindings` | `workflow_id` | — | Lists the paired chats bound to the bot | a Channel / Sender id table, or a note that bindings are not enabled yet | | `bind`, `unbind` | `workflow_id`, `channel`, `sender_id` | — | Binds a chat you already paired to the bot, or releases it | a confirmation, or ``Error: … (not_paired)`` | | `groups` | — | — | Lists your bot groups | a Name / Members / ID table | | `group_set` | `group`, `members` (2–6 bot ids) | — | Creates the group, or replaces the members of the one with that name | "Created" / "Updated group …" | | `group_delete`, `group_thread` | `group` | — | Deletes the group, or reads its shared thread | a confirmation, or the thread table | **Validation.** On `create`, and on `update` with `yaml`, the YAML goes through the engine's CNL validator (`yamlToCNL`) before anything is stored. An error rejects the call: "Workflow YAML failed CNL validation: \<field\>: \<message\>, …" (`isError`), and nothing is saved. Warnings (near-miss keys such as `input` on a task where `prompt` was meant) are appended as "⚠️ Validation warnings:" and do not block. The same check runs the save-time kind lint: `memory_owner` in the YAML is an error for every kind (memory tenancy comes from the executing user), and an `agent` or `bot` must be exactly one `type: harness` task whose `allowed_tools` (if set) include `task_complete`. The kind linted is the `kind` you pass, else the stored one on `update`. Hardcoded emails or UUIDs only warn. The validator checks the schema, not the agents. A saved workflow can name an inactive agent and fail only when it runs, so take ids from [Discovery](/mcp/discovery). **Folders.** `folder` is matched against your folder names: an exact case-insensitive match, otherwise a *unique* partial match. A name that matches nothing **creates that folder** (it appears in Studio at once) and files the workflow into it, on `create`, `update` and `templates use` alike. A name that partially matches two or more folders is an error naming them ("…is ambiguous — it matched 2 folders: Sales East, Sales West. Use the full name."), and nothing is changed. `folder: "none"` (or `""`) on `update` moves the workflow out of its folder. **Kind.** `kind` picks the sidebar section: `workflow` (Flows, the default), `agent` (Agents), `bot` (Bot agents) or `campaign` (a list worked through on a cycle). A `campaign` create takes `campaign_config` (`{list, cycle, max_attempts, schedule?, deliver?, …}`): the config is checked before anything is saved, then the ledger and the scheduler job are set up and the campaign id is written into the YAML. Without `campaign_config` the campaign is saved with a warning and no ledger. **Bot control.** The bot actions work on a saved bot you own and use the same stores as Studio's bot settings and the One API's `/v1/workflows/{id}/permissions`, `…/workspace-files`, `…/inbox`, `…/bindings` and `/v1/bot-groups`; a refusal reads ``Error: <message> (<code>)`` with the One API's code. Bindings need the message gateway and groups need their table: until then they answer a plain "not enabled yet". **Errors.** A missing required field answers "Error: \<field\> is required for action "\<action\>"" (`isError`). An id that does not exist or is not yours answers "Workflow not found." or "Version not found." (`isError`), except `get`, which returns a `needs_input` outcome ("Workflow `<id>` not found.") pointing at `plungeai_list_workflows`. The bot actions (`permissions_get`, `files`, `inbox`, `bindings`, …) answer ``Error: no saved workflow with that id for this account (workflow_not_found)`` instead. **Two things to know.** `update` does **not** save a version first (the same as Studio), so run `save_version` before an edit you may want to undo. `delete` is a hard delete of the workflow. Your past executions of it stay in your history. The clean-up that follows is best effort and never fails the call: for a bot it wipes only the memory namespace `<you>:bot:<id>` (older bots; a bot saved without `memory_owner` uses the memory of whoever runs it, which a delete never touches) and deletes the bot's scheduler jobs; for a campaign it deletes the jobs and tombstones the ledger. Changing a saved row's `kind` away from `bot` or `campaign` with `update` runs the same clean-up for the old kind. ### Try asking - *"Show me my Acquisition Scanner workflow — what does it actually do?"* The client finds its id with `plungeai_list_workflows` and fetches the definition with `get`, so you can see exactly what it runs (which agents, in what order) before you trust it with real input. - *"Rename my Acquisition Scanner, move it to the Finance folder, and save a version first."* The client runs `save_version`, then `update` with `name` and `folder`. - *"Create a workflow from this YAML and put it in Finance."* `create` files it straight into the folder; no separate move is needed. - *"What is my Outreach bot allowed to do without asking?"* The client runs `permissions_get`; *"Make it ask before sending anything"* is `permissions_set` with `classes: {"send": "ask"}`. - *"Group my Outreach and Research bots as Sales."* `group_set` with `group: "Sales"` and the two bot ids; `group_thread` reads what they said to each other. Reading a definition (read-only; it needs a real id, so it has no verifier marker): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_workflow","arguments":{"user_request":"Show me my Acquisition Scanner workflow","action":"get","workflow_id":"<workflow id>"}}}' ``` Creating one into a folder changes your account (not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_workflow","arguments":{"user_request":"Save this as News Scan in my Finance folder","action":"create","name":"News Scan","folder":"Finance","description":"Latest news on a company","yaml":"name: News Scan\ntasks:\n - id: news\n agent: brave-agent\n prompt: \"{input} latest news\"\n"}}}' ``` Reading a bot's policy is read-only (it needs a real id, so it has no verifier marker); `permissions_set` takes the same arguments plus the fields to change and writes them, and `bindings`, `files`, `inbox` and `groups` read the bot's paired chats, workspace, queued messages and groups the same way: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_workflow","arguments":{"user_request":"What is my Outreach bot allowed to do","action":"permissions_get","workflow_id":"<bot id>"}}}' ``` Creating a bot group changes your account (not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_workflow","arguments":{"user_request":"Group my Outreach and Research bots as Sales","action":"group_set","group":"Sales","members":["<bot id>","<bot id>"]}}}' ``` The same workflow is also readable as the resource `plungeai://workflows/{id}` ([Prompts & resources](/mcp/prompts-resources)). ### A2A doors Every bot, agentic agent and workflow can get an **Agent2Agent (A2A 1.0) door**: a URL under `https://a2a.plungeai.com/<handle>` with its own bearer token, through which any A2A client — another vendor's agent, the official `a2a-sdk` clients, or one of your own bots — sends it work. `plungeai_workflow` `action: "a2a_door"` manages it: `door_action: "mint"` creates the door and shows the token once (minting again rotates it), `get` shows the door, `pause` / `resume` close and reopen it, `revoke` deletes it. The door's agent card is at `<door url>/.well-known/agent-card.json`; a `SendMessage` runs the workflow as you and returns the result as a text artifact (bots and agentic agents continue the conversation when the caller reuses the `contextId`; a session that stops with a question answers `TASK_STATE_INPUT_REQUIRED`). Long work: send with `returnImmediately` or stream, then poll `GetTask`. A bot reaches another A2A agent with `message_bot` and a `peers` entry `a2a:<url>`; its bearer for that agent is the Connections credential `a2a:<host>` (or `a2a:<host>/<path>` for one door on a host). ## 8.7 Versions Versions are explicit snapshots (`workflow-store.ts`): - `save_version` copies the **current live YAML** into a new version numbered one above the highest so far (V1, V2, …), with an optional `version_description` note and a checksum. It becomes the workflow's current version. - `list_versions` shows every snapshot: Version, ID (the `version_id`), When (ISO time), Note. - `get_version` returns one snapshot's YAML without changing anything. - `restore_version` writes that snapshot's YAML back as the live definition, then saves it as a **new** version with the note "Restored from V\<n\>". History is never rewritten, and you can always roll forward again. `update` and `plungeai_build_workflow`'s refine both overwrite the live YAML without a snapshot. Save a version first so you can roll back. ## 8.8 Generate a workflow — `plungeai_build_workflow` `plungeai_build_workflow` turns a plain-language goal into a real multi-agent workflow and saves it. It uses the platform's workflow builder, which picks actual registry agents, validates the YAML and repairs it itself. | You send | Does | |---|---| | `goal` | Generates a **new** workflow and saves it. It is named from the generated YAML's own `name:` line (up to 120 characters), or from the first 60 characters of your goal when the YAML has no plain `name: value` line (a name containing a quote or apostrophe does not count as one), with the description "Generated via MCP builder", no folder, kind `workflow` | | `workflow_id` + `instruction` | **Refines** an existing workflow: sends its current YAML plus your change to the builder, which regenerates the whole workflow, then overwrites it in place. The name stays; no version is saved | | anything else | "Provide either `goal` (new) or `workflow_id` + `instruction` (refine)." (`isError`) | If you send `goal` together with `workflow_id` but no `instruction`, a new workflow is generated and the id is **ignored**. It takes 30 to 60 seconds, and it narrates while it works: ``` 📖 Loading the existing workflow… (refine only) 🧠 Generating CNL YAML — the builder picks real agents, validates and self-repairs (30-60s)… 🔍 Validating the generated workflow (2,431 chars)… 💾 Saving "Acquisition News Briefing" and syncing to all apps… ✅ Complete ``` The answer names the workflow and its id, says "Synced to all apps.", and, for a new one, "Run it with plungeai_execute_workflow.", followed by the full YAML in a code block. It fails with `isError` and saves nothing when: - the workflow id is unknown ("Workflow not found: \<id\>"); - the builder returns no workflow ("Workflow builder did not return a workflow section."); - the generated YAML fails CNL validation ("Generated workflow failed CNL validation: …", with the YAML attached so you can fix it by hand). The tool has no `format` argument. *Try asking: "Build me a workflow that scans a company for acquisition news and writes a briefing."* To file it, follow up with *"…and move it to Finance"* (`plungeai_workflow` `update` with `folder`). To design a workflow in a conversation first, *"Let's design a due-diligence workflow together"* goes to `plungeai_chat` ([Chat, memory & skills](/mcp/chat-memory-skills)). This runs the builder model and saves a workflow (it costs credits; not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_build_workflow","arguments":{"user_request":"Build me a workflow that scans a company for acquisition news and writes a briefing.","goal":"Scan a company (given as input) for recent acquisition news from several web sources in parallel, then write a one-page briefing with sources."}}}' ``` To refine: `{"workflow_id":"<id>","instruction":"Add a step that checks SEC filings for the same company."}`. Run `save_version` first. ## 8.9 Start from a template — `plungeai_templates` `use` `use` copies a template ([5.5](/mcp/discovery#55-workflow-templates--plungeai_templates-list-and-get)) into your account as a new workflow you can edit: - The name is your `name`, otherwise "\<template name\> (copy)". A `bot` template keeps its own name. - The template's kind (`agent` or `bot`) and its description carry over. - `folder` files it with the same matching as `plungeai_workflow` `create`: no match means the workflow is created without a folder, with a ⚠️ note listing your folders. - The template's usage count goes up by one, the same as in Studio. - The YAML is copied as it is. It is not re-validated. The answer: "Created workflow **\<name\>** (`<id>`) from template **\<template\>**. Filed under **\<folder\>**. Run it with plungeai_execute_workflow." *Try asking: "What workflow templates are available? Start one from the due-diligence template."* This creates a workflow in your account (not run by the docs verifier): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_templates","arguments":{"user_request":"Start one from the due-diligence template","action":"use","template_id":"<template id from list>","name":"Due Diligence — Acme","folder":"Finance"}}}' ``` ## 8.10 CNL essentials for ad-hoc YAML The full language reference is the `plungeai-workflows` skill (https://skills.plungeai.com). The same guide is served to your client as the `plungeai://docs/workflow-authoring` resource ([Prompts & resources](/mcp/prompts-resources)). It is also summarised at https://api.plungeai.com/docs#101-cnl-in-one-page. The short version (`cnl-spec.md`, engine v6): - **Shape.** `workflow: {name, tasks, inputs?}` (Form A) or bare `name` + `tasks` at the top level (Form B). Both are accepted. The spec's Form B top-level `input:` is not read over MCP: the gateway nests the whole Form B document under `workflow:`, where nothing reads `input`. Pass the value as the tool's `input` argument instead, or declare `inputs:` inside a Form A `workflow:` block. - **Ten task types.** `task` (one agent: `agent` + `prompt` or `query`), `parallel` and `sequential` (`subtasks`), `dynamic` (`generator` + `executors`), `batch` (`tasks` over `items` / `items_from`), `output`, `debate`, `validate`, `agent_loop` (legacy), `harness` (a mission: `goal` + `mission` or `mission_ref`). - **Interpolation.** `{input}`, `{key}` from `inputs`, `{data:task_id}` for a prior task's result, `{item}` / `{item.field}` inside a batch. - **Hard rules.** Agent ids exactly as in the registry. No `$variable`, `depends_on`, `outputs:` or `parallel: true`. Container blocks never carry `agent:`. `type: task` reads `prompt:`, not `input` or `instructions`. Unique `id`s. A mission as YAML: a `type: harness` task with the full mission fields, including experts, plugins, MCP servers, effort, spend cap and permissions, which `plungeai_run_mission` does not expose ([7.7](/mcp/agents-missions#77-mission-bounds)): ```yaml name: Vendor scan tasks: - type: harness id: market_scan goal: "Map the top 5 vendors for {input}, with pricing and one differentiator each." mission: mission: "You are a market analyst. Research thoroughly, cite sources, stay on topic." effort: standard max_turns: 12 allowed_tools: [web_search, web_fetch, task_complete] success_criteria: - "5 vendors named with pricing" - "every claim has a source URL" ``` Run it with `plungeai_execute_workflow`, passing the YAML as `workflow_yaml` and the value for `{input}` as the tool's `input` argument (for example `"input":"workflow orchestration platforms"`). # Runs, results & follow-ups Source: https://www.plungeai.com/docs/mcp/runs-results Source: `orchestration/mcp-gateway/tools.ts` (`getResult`, `getWorkflowStatus`, `plungeaiExecutions`, `plungeaiFollowup`, `plungeaiContinue`), `orchestration/mcp-gateway/ops/executions.ts` (`reconcileRunningRow`), `orchestration/mcp-gateway/conversation.ts`, `orchestration/mcp-gateway/execution.ts` (`loadStoredResult`, `STALE_RUN_SECONDS`), `orchestration/mcp-gateway/leg-state.ts`, `orchestration/mcp-gateway/tool-outcome.ts` (`pauseOutcome`, `renderAs`), `orchestration/mcp-gateway/server.ts` (`TOOL_DEFS`, `StatusOutputSchema`, server instructions). Every execution tool (`plungeai_execute_workflow`, `plungeai_execute_agent`, `plungeai_execute_tool`, `plungeai_run_mission`, `plungeai_learn`) creates an execution and answers with its **execution id**. This chapter covers what you do with that id afterwards: read the output, check the status, browse your history, ask a follow-up, and answer a run that paused for you. The examples use `https://mcp.plungeai.com/v1`. `https://api.plungeai.com/v1/mcp` is the same server. | Tool | What it does | Read-only | Runs a model | |---|---|---|---| | `plungeai_get_result` | the output of one execution, or of one task in it | yes | no | | `plungeai_get_workflow_status` | status, error, duration, and whether the run is paused for you | yes | no | | `plungeai_executions` | your run history; also output, thread, `files`, `steer`, `cancel`, `delete` | no (`steer`, `cancel` and `delete` change state) | no | | `plungeai_followup` | a new turn on an execution: continues it, or steers it while it runs | no | yes | | `plungeai_continue` | answers a paused run, or approves its staged action | no | yes | Full parameter tables are in the [Tool reference](/mcp/tool-reference). ## 9.1 Execution ids **Where the id comes from.** An `ok` answer ends with a footer line: ``` --- execution: `<execution_id>` — use this id for plungeai_get_result / plungeai_followup on this run. ``` A guidance answer for a run that did start (`needs_approval`, a paused `needs_input`, a classified failure) ends with `_(execution: <execution_id>)_`. A `mode:"async"` call returns the id at once. With `format:"json"` the id is the outcome's `execution_id` field. **An execution id is not a workflow id.** A workflow id names the recipe, a saved workflow. Every run of it gets a new execution id. The result and status tools need the execution id. If you only have the workflow, list its runs with `plungeai_executions {action:"list", workflow_id:"<workflow_id>"}` and take the id from there. **Which forms are accepted.** | Id | Example shape | Accepted by | |---|---|---| | execution id | a UUID | every tool in this chapter | | engine id | `exec-…` (hyphen), as scheduler surfaces show it | `plungeai_get_workflow_status`, `plungeai_get_result`, `plungeai_executions` `get` / `output` / `cancel`. `plungeai_executions` `delete` and `conversation` need the execution id itself. `plungeai_followup` and `plungeai_continue` resolve an `exec-…` id but save the new turn under it, so the thread does not show in `plungeai_get_result` or Studio (and a follow-up misses the run's stored result as context): pass them the execution id. `plungeai_executions {action:"list"}` shows it in the ID column; `plungeai_get_workflow_status` does not (its `execution_id` echoes what you passed) | | scheduler run id | `exec_…` (underscore), from old scheduled runs | none. `plungeai_get_result` and `plungeai_get_workflow_status` answer with a pointer to `plungeai_schedule {action:"runs"}`, whose Execution ID column gives the id to use ([Scheduling](/mcp/scheduling)) | **Ownership.** Every tool here checks that the execution belongs to the key's user before it reads anything. You see your own runs only. **"Show me #2."** A numbered reference means the Nth row of a list as you saw it. The server instructions tell clients to resolve it against the list already shown in the conversation, never against a freshly fetched one: new runs land all the time and shift the newest-first numbering. **A refusal is not a run.** A call stopped before dispatch (missing field, unknown agent, unknown model) creates no execution and has no id. See [Errors, feedback & limits](/mcp/errors-limits). ## 9.2 Get a result: plungeai_get_result Pass `execution_id`. `workflow_id` is accepted as an older alias, and it still means an execution id. Add `task_id` to read one task's output instead of the final result. A full result is assembled in this order: 1. **An identity line.** The workflow name and the first 80 characters of the question, then the start time in UTC and the first 8 characters of the execution id. People rerun the same question often; this line says which run you are looking at. 2. **The final output.** Finished Markdown, as the run produced it. 3. **Steps in this run (N).** Only for runs with two or more steps: each step's task id and the agent that ran it, with the final step marked. Any step is one call away with `task_id`. 4. **Conversation thread (N messages).** Only when the run has follow-up or continue turns: the whole thread, as `**User:**` / `**Agent:**` turns. 5. **A ⏸ block** when the run is paused and waiting for you ([9.6](#96-paused-runs-plungeai_continue)). The server instructions tell clients to show this output verbatim and in full. Some chat clients still summarize long answers; asking for it "verbatim, don't summarize" stops that. **Where results live.** When a run finishes, its final output is saved with the execution and does not expire; Studio and this server write the same record. When that saved copy is missing (an older run, or a failed save), the final output is read from the run's shared memory instead, which does expire. The outputs of the other steps live only in shared memory. So `task_id` for an old run can come back empty while the full result still reads fine. **One step.** `task_id` for a non-final step reads that step's own output (plus a ⏸ block if the run is paused). Passing the final step's id returns the full result. If the step output is gone, the answer is `No result found for workflow=<id>, task=<task_id>. The task may still be running, or its output has expired from SharedMemory.` (`isError: true`). **When there is no output**, the answer says why. Before it answers, the tool self-heals a stuck `running` row the same way the status tool does ([9.3](#93-check-status-plungeai_get_workflow_status)). | Situation | Answer | |---|---| | No id passed | 📝 NEEDS INPUT: ``Provide the id of the execution to fetch (as `execution_id`).`` | | Unknown id, or not yours | 📝 NEEDS INPUT: ``No result found for `<id>`, or it is not one of your executions.`` The remediation points to `plungeai_executions {action:"list"}` | | Scheduler run id `exec_…` | `That is a scheduler run id, which this tool cannot resolve. Use plungeai_schedule {action: "runs"} …` (`isError: true`) | | The run failed | `This run failed: <reason>. (execution: <id>)` (`isError: true`). Fix the input and rerun; polling will not change it | | Still running | `No result yet for <id> — it is still running. Poll plungeai_get_workflow_status, then retry.` (`isError: true`) | | Completed, but nothing is stored any more | `Run <id> completed, but its stored result is no longer retrievable (expired from SharedMemory).` (`isError: true`) | **JSON.** With `format:"json"` a result is the body `GET /v1/executions/{id}/output` returns on the One API, field for field: `execution_id`, `status` (`needs_input` / `needs_approval` while the run waits on you), `content` (the stored output, raw), `final_task_id`, `steps` (`task_id`, `agent`, `final`) and `continuation`, in the text and in `structuredContent`; it carries no rendered thread. With `task_id` it is `{execution_id, task_id, result}`. The not-ready answers above stay `{"content": "<the same text>"}`, and a NEEDS INPUT answer becomes the outcome object itself. Try asking: - *"Show me the result of that last run."* - *"Show me the full result for that specific task, not just the summary."* Your client takes the task id from the **Steps in this run** index. The full result of one run (replace `<execution_id>`): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_get_result","arguments":{"user_request":"show me the result of that last run","execution_id":"<execution_id>"}}}' ``` One step of it: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_get_result","arguments":{"user_request":"show me the full output of the search step","execution_id":"<execution_id>","task_id":"<task_id>"}}}' ``` An id that is not yours comes back as guidance, not an error: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_get_result","arguments":{"user_request":"show me that result","execution_id":"00000000-0000-4000-8000-000000000003"}}}' ``` The same output is also readable as the resource `plungeai://executions/{id}` ([Prompts & resources](/mcp/prompts-resources)). ## 9.3 Check status: plungeai_get_workflow_status Pass `execution_id`. The tool has no `format` parameter: every answer carries both Markdown and `structuredContent`, which matches the tool's declared `outputSchema`. The Markdown answer: ``` # Execution <execution_id> - **Workflow:** <name> - **Status:** <status> - **Error:** <reason> (failed runs only) - **Note:** The run stopped at its turn budget before finishing; the partial output is retrievable. (incomplete only) - **Started:** <ISO time> - **Duration:** <n>ms - **Final task:** <task_id> Fetch output with plungeai_get_result. (completed or incomplete) ``` A ⏸ block follows when the run is paused ([9.6](#96-paused-runs-plungeai_continue)). **Status values.** | `status` | Meaning | What to do | |---|---|---| | `running` | still executing | poll again | | `completed` | finished; the output is stored | `plungeai_get_result` | | `incomplete` | stopped at its turn or iteration budget before finishing; the partial output is stored (async runs only, below) | `plungeai_get_result` reads the partial output. Rerun with a larger budget if you need more | | `failed` | failed; `error_message` carries the reason (null on the one poll that self-heals the row, below) | fix the input and rerun. Do not keep polling | | `cancelled` | stopped with `plungeai_executions {action:"cancel"}` | nothing; rerun if you still want it | An `incomplete` run is stored as `completed` with an `incomplete` outcome. This tool reports it as `incomplete` so a status-only poller never reads it as success. The history list ([9.4](#94-run-history-plungeai_executions)) shows the stored `completed`. This holds for `mode:"async"` runs only. A sync execute or mission call stamps the outcome `ok` when it answers, which replaces the `incomplete` mark, so the same run then reports `completed` here, and its answer reads as a normal success ([7.7](/mcp/agents-missions#77-mission-bounds), [8.5](/mcp/workflows#85-what-a-run-returns)). This is current behaviour. Use async when you need to tell a finished run from one that ran out of turns. **Self-heal.** Each read of a `running` row checks it first: - the run's output exists but the row still says `running` → the row is set to `completed`; - the row has had no heartbeat for more than 960 seconds (16 minutes) → it is set to `failed` with the reason `Interrupted (run did not complete)`. The rule keys on the last heartbeat, not the start time. A live run keeps heartbeating, so a long run is not cut off by it. A background run that died without finishing is reported as failed instead of reading "running" forever. The answer that performs this fix already says `failed`, but its `error_message` is still null (and `plungeai_get_result` says `no error was recorded`); the next read carries `Interrupted (run did not complete)`. **`structuredContent`** (the registered `outputSchema`): | Field | Type | Notes | |---|---|---| | `execution_id` | string | the id you passed | | `status` | string | one of the values above | | `error_message` | string or null | set for `failed` runs only | | `workflow_name` | string or null | the display name, e.g. `<name> · <agents that ran>` | | `started_at` | string or null | ISO 8601 | | `duration_ms` | number or null | | | `final_task_id` | string or null | | | `continuation` | object or null | non-null when the run is paused for you: `{ status, agent, question, pending_action }`. `status` is `needs_approval` or `question`; `question` is the question or null; `pending_action` is `{ summary, price? }` or null | **Errors** (all `isError: true`): | Answer | Cause | |---|---| | `Execution not found.` | no execution with this id is yours (an unknown id and another account's id read the same) | | `That is a scheduler run id, which this tool cannot resolve. …` | an `exec_…` id; use `plungeai_schedule {action:"runs"}` | | ❌ ERROR `Status storage is temporarily unavailable — please retry in a moment. (request <id>)` | a storage hiccup; retry once after a few seconds | **Polling.** A `mode:"async"` run sends no live progress, so poll this tool until the status leaves `running`. Then fetch the output with `plungeai_get_result`. A paused background run shows up here as a `continuation`, so it never sits unseen. Try asking: *"Is that still running?"* ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_get_workflow_status","arguments":{"user_request":"is that still running?","execution_id":"<execution_id>"}}}' ``` An unknown id: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_get_workflow_status","arguments":{"user_request":"is that still running?","execution_id":"00000000-0000-4000-8000-000000000003"}}}' ``` ## 9.4 Run history: plungeai_executions One tool with eight actions. Every action except `list` needs `execution_id`. | `action` | Does | Markdown answer | With `format:"json"` | |---|---|---|---| | `list` | your runs, newest first | `# Executions (N)` and a table: ID, Question (first 60 characters), Workflow, Status, Source, When | `{"executions": [{id, input, workflow_name, status, start_time, duration, source}]}`; `start_time` is Unix seconds, `duration` is milliseconds, `source` is the surface that started the run (`null` when unrecorded) | | `get` | the same answer as `plungeai_get_workflow_status` | the status block | the status object | | `output` | the same full result as `plungeai_get_result` | the result | `{"content": "<markdown>"}` | | `conversation` | the run's follow-up thread only | `# Conversation — <id>` and the turns, or `No conversation thread for this execution yet.` | `{"execution_id", "messages": [...]}`, or `{"messages": []}` | | `files` | the files the run wrote | `# Files of <id> (N)` and a table: Path, Size, Type, or `This run wrote no files.` | `{"content": "…"}` | | `steer` | queues `message` for a running execution | ``Queued your message to running execution `<id>` (delivered at its next turn).`` | `{"content": "…"}` | | `cancel` | stops a running execution | ``Cancelling execution `<id>` — the run stops at its next turn or tool boundary.`` | `{"content": "…"}` | | `delete` | deletes the execution record and its stored result | ``Deleted execution `<id>`.`` | `{"content": "…"}` | **`list`.** `limit` defaults to 20 (1–100); `offset` defaults to 0 (up to 100,000). `workflow_id` narrows the list to runs of one workflow. The Status column shows the stored status, so an `incomplete` run shows `completed` here; `get` gives the honest status. The Source column names the surface that started the run: `mcp` (this server), `api` (the One API), `cli` (either of those, called with the header `X-Ocean-Client: cli`), `studio`, `mini-app`, `scheduler` or `campaign-agent`; older rows without one show `—`. **`cancel`** works on a `running` execution only. Anything else answers `Execution is <status>, not running — nothing to cancel.` (`isError: true`). The cancel is cooperative: the tool writes a cancel marker that the engine and the harness read at their next safe boundary, and it sets the row to `cancelled` (reason `Cancelled by user`) at once. A step that finishes late does not relabel the row. **`steer`** works on a `running` execution only: the message is queued for the live loop and delivered at its next turn. A finished run answers ``Execution is <status>, not running — nothing to steer.`` (`isError: true`); continue a finished run with `plungeai_followup` ([9.5](#95-follow-ups-plungeai_followup)). **`delete`** cannot be undone. It removes the execution row and its stored result, and tells connected apps (Studio and the others) that the run is gone. **Errors.** `execution_id is required for this action.` and `Execution not found.` (unknown, or not yours) are `isError: true`. A storage hiccup while reading a thread comes back as the ❌ ERROR envelope with a request id. Try asking: - *"Show me my last 10 executions."* - *"Cancel that run."* - *"What files did that run write?"* The client runs `files`. - *"Tell the running bot to skip the Berlin leads."* The client runs `steer` with the message. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_executions","arguments":{"user_request":"show me my last 10 executions","action":"list","limit":10,"format":"json"}}}' ``` Cancelling a run changes its state (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_executions","arguments":{"user_request":"cancel that run","action":"cancel","execution_id":"<execution_id>"}}}' ``` Listing the files a run wrote is read-only (it needs a real id, so it has no verifier marker): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_executions","arguments":{"user_request":"what files did that run write","action":"files","execution_id":"<execution_id>"}}}' ``` Steering a running bot or agent queues a message for its next turn and changes the run (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_executions","arguments":{"user_request":"tell the running bot to skip the Berlin leads","action":"steer","execution_id":"<execution_id>","message":"Skip the Berlin leads."}}}' ``` ## 9.5 Follow-ups: plungeai_followup A follow-up keeps a run's context instead of starting over. Pass `execution_id` and `prompt`. What happens depends on the state of the run: | State of the run | What happens | Answer | |---|---|---| | still `running` | your message is queued into the live run and delivered at its next turn (steering). No new run starts | ``Queued your message to running execution `<id>` (delivered at its next turn).`` | | finished, with an open agent session | the same agent continues its conversation with your message | the agent's reply, plus a ⏸ block if it pauses again | | finished, no session | the researcher fallback: `harness-agent` answers with the run's stored result as context | the answer | **The researcher fallback** takes the first 9,000 characters of the run's stored output as prior context. It runs with the tools `recall`, `recall_history`, `knowledge`, `web_search`, `web_fetch`, `registry_search` and `task_complete`, for up to 12 turns. It is told to verify any new factual question with web search, cite source URLs, and never answer from the prior context alone. Because the stored result does not expire, an old run can still take follow-ups, when you pass the execution id itself ([9.1](#91-execution-ids)). **Both turns are saved.** Your message and the answer are appended to the run's conversation thread. When you passed the execution id, the thread shows up in `plungeai_get_result`, in `plungeai_executions {action:"conversation"}`, and in Studio. **It narrates itself.** While it works it sends live status on the progress channel ([Protocol & transport](/mcp/protocol)): ``` 🔎 Looking up the run and checking for an open session… 💬 <agent> working — continuing the conversation… (open session) 📚 No open session — loading the run's stored result as context… (fallback) 🔬 Researching with harness-agent — <n> chars of prior context… ⏳ Collecting the answer… 💾 Saving this turn to the conversation thread… ✅ Complete — continued the existing session ``` The answer is plain Markdown text, not the outcome envelope, and the tool has no `format` parameter. Errors are `isError: true`: `Execution not found.`, `Followup agent call failed`, `Failed to retrieve follow-up response`. Follow-up or continue? A follow-up is for a finished run, or for steering a running one. A run that stopped to ask you something is answered with `plungeai_continue` ([9.6](#96-paused-runs-plungeai_continue)). Try asking: *"Now do the same analysis but focus on the competitive landscape."* This runs a model and costs credits (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_followup","arguments":{"user_request":"now do the same analysis but focus on the competitive landscape","execution_id":"<execution_id>","prompt":"Do the same analysis, but focus on the competitive landscape."}}}' ``` ## 9.6 Paused runs: plungeai_continue Some agents stop instead of finishing: always before spending money, and sometimes to ask a question ("which wallet?", "what should the call say?"). Nothing has been executed when a run pauses. **How a pause shows up.** - On the run's own answer (the sync execute tools): the outcome `needs_approval` with the summary `⏸ AWAITING USER APPROVAL: <action> (<price>). Relay this to the user verbatim and ask them to decide — NEVER approve on your own.`, or the outcome `needs_input` with `⏸ AWAITING USER: <question>`. The remediation names `plungeai_continue` and the execution id. - On `plungeai_get_result`, `plungeai_get_workflow_status` and follow-ups: a closing block. ``` --- ⏸ AWAITING USER APPROVAL: <action> (<price>) Ask the user to confirm, then call plungeai_continue with execution_id "<id>" and approve: true (or message: "<their words>"). ``` For a question: `⏸ AWAITING USER: <question>` and `Relay this to the user, then call plungeai_continue with execution_id "<id>" and message: "<their answer>".` - In the status tool's `structuredContent.continuation` ([9.3](#93-check-status-plungeai_get_workflow_status)). This is how `mode:"async"` runs surface a pause. **The loop.** 1. The run comes back paused. Nothing has run yet. 2. Your client relays the ⏸ block and asks you to decide. The server instructions tell it never to approve on its own. 3. You answer in your own words. *"Yes"* or *"approve"* becomes `approve: true`. Anything else, including *"no"*, goes back as `message`. 4. On approve, the held action runs exactly once. A repeated or stale approve is harmless: the agent answers that nothing is awaiting approval, so it can never pay twice. **Parameters.** `execution_id` plus `message`, or `approve: true`. If you pass both, the approval is sent. `approve: false` on its own is rejected; to deny, pass the user's words as `message`. **What comes back.** The agent's reply, then either a new ⏸ block (it paused again) or `✅ Conversation resolved (no further input needed).` The tool waits up to about 30 seconds for the reply, because an approved payment or outside call runs during this turn. The turn is saved to the run's thread; an approval is recorded as `✅ Approved`. **Errors** (all `isError: true`): | Answer | Cause | What to do | |---|---|---| | `` Provide `message` or `approve: true` `` | neither was passed | pass one | | `Execution not found` | unknown id, or not yours | check the id | | `No conversation session for this execution (expired or not a conversational agent)` | the run holds no paused session | nothing to continue; use `plungeai_followup` for more on a finished run | | `This run isn't paused — it already completed. Use plungeai_followup to ask a follow-up about it.` | the session is finished and you sent a `message` | use `plungeai_followup` | | `Agent continuation failed` | the engine call failed, or no reply arrived in the ~30-second window | check `plungeai_get_workflow_status` and `plungeai_get_result` before you retry: an approved action may already have run | **The trust fence is different.** A structured tool called directly with `plungeai_execute_tool` (or a workflow task) for an outward, irreversible operation (send, pay, transfer, buy, withdraw) comes back ⏸ NEEDS APPROVAL from the engine's fence, with nothing run, no `approve_via` action and no paused session. `plungeai_continue` does not apply to it (it answers `No conversation session…`). Relay it to the user: such an operation runs only from an interactive session such as Ocean Studio, where a person can approve it. See [6.6](/mcp/tools#66-the-trust-fence--needs-approval). Try asking: *"Yes, go ahead"* / *"Use the second option"*. Approve, only after the user said yes. This runs the held action, which may spend money (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_continue","arguments":{"user_request":"yes, go ahead","execution_id":"<execution_id>","approve":true}}}' ``` Answer a question instead: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_continue","arguments":{"user_request":"use the second option","execution_id":"<execution_id>","message":"Use the second option."}}}' ``` ## 9.7 Which tool when | You want | Call | |---|---| | the answer of a run | `plungeai_get_result {execution_id}` | | one step's full output | `plungeai_get_result {execution_id, task_id}`; the ids are in **Steps in this run** | | to know if it is done, failed or paused | `plungeai_get_workflow_status {execution_id}` | | your recent runs, or the runs of one workflow | `plungeai_executions {action:"list"}` (with `workflow_id` to filter) | | to stop a running run | `plungeai_executions {action:"cancel", execution_id}` | | more on a finished run, with its context | `plungeai_followup {execution_id, prompt}` | | to change course while it still runs | `plungeai_followup` (it queues the message into the run) | | to answer a ⏸ block | `plungeai_continue {execution_id, message}` or `{approve: true}` | # Chat, memory & skills Source: https://www.plungeai.com/docs/mcp/chat-memory-skills Source: `orchestration/mcp-gateway/tools.ts` (`ChatSchema`, `plungeaiChat`, `MemorySchema`, `plungeaiMemory`, `LearnSchema`, `plungeaiLearn`), `orchestration/mcp-gateway/chat.ts`, `orchestration/mcp-gateway/tools-bots.ts` (the bot actions of 10.4), `orchestration/mcp-gateway/extras.ts` (`registrySigned`), `orchestration/mcp-gateway/index.ts` (argument pre-screen), `orchestration/mcp-gateway/server.ts` (`TOOL_DEFS`), `core/core-memory/core-memory.ts` and `core/core-memory/CLAUDE.md`, `agents/agents/harness-agent/runtime.ts` (run journal write). Three tools keep something for you across calls: a conversation, a memory, a skill. The examples use `https://mcp.plungeai.com/v1`. `https://api.plungeai.com/v1/mcp` is the same server. | Tool | Keeps | Also seen in | Read-only | |---|---|---|---| | `plungeai_chat` | conversations with the PlungeAI assistant | Studio's Think tab, `ocean-cli chat sessions` | no | | `plungeai_memory` | your long-term memory (notes and a user profile); also reads the run journal | every mission recalls it at start; Studio's harness uses the same store | no (`remember` writes) | | `plungeai_learn` | your private skill library | registry skill cards only you can see | no (`learn` and `forget` change state) | Full parameter tables are in the [Tool reference](/mcp/tool-reference). ## 10.1 Chat: plungeai_chat A persistent conversation with the platform assistant. One tool, four actions: | `action` | Needs | Does | Answer | |---|---|---|---| | `send` | `message`; optional `conversation_id` | sends one turn and waits for the whole reply | the reply, then ``_(conversation: `<id>`)_`` | | `new` | optional `message` | creates an empty conversation; `message` only sets its title | ``Created conversation `<id>`. Send messages with action "send" and this conversation_id.`` | | `list_sessions` | nothing | your 20 most recently updated conversations | `# Your conversations (N)` and a table: Title, ID, Messages. Or `No conversations yet.` | | `history` | `conversation_id` | the whole conversation | `# <title>` and every turn as `**User:**` / `**Assistant:**` | **The assistant** is `harness-agent` on Anthropic `claude-sonnet-4-5`, with up to 15 turns and 8,192 output tokens per reply. Its tools are `web_search`, `registry_search`, `registry_lookup` and `task_complete`. It is told to verify current facts with web search, cite source URLs, and use the registry to find platform agents when relevant. It cannot run workflows or save anything: it has no execute or save tool. When a design is settled, ask your client to build it, and it calls `plungeai_build_workflow` ([Workflows](/mcp/workflows)). **One account, three surfaces.** A conversation is stored the way Studio stores its plan conversations. The same conversation shows up in Studio's Think tab and in the CLI's `ocean-cli chat sessions`, and a thread started there can be continued here by its id. **Titles.** `send` without a `conversation_id` starts a new conversation titled with the first 50 characters of your message. `new` titles it with the first 50 characters of `message`, or `New conversation` when there is none. That title stays: a later first `send` does not rename it. **Live status.** A `send` reports `Thinking…`, then `Streaming… <n> chars` as the reply arrives ([Protocol & transport](/mcp/protocol)). **Errors** (all `isError: true`): | Answer | Cause | |---|---| | `message is required for send.` | `send` without `message` | | `conversation_id is required for history.` | `history` without an id | | `Conversation not found.` | `history` for an unknown id, or one that is not yours | | `Chat error: Conversation not found or not yours` | `send` to an unknown or foreign `conversation_id` | | `Chat error: Harness agent error (HTTP <n>): …` | the assistant could not be reached | **One writer per conversation.** Two `send` calls on the same conversation at the same time can lose a turn: the conversation store has no compare-and-swap. Send turns one after another. **Chat or something else?** | You want | Use | |---|---| | an open-ended thread with the platform assistant | `plungeai_chat` | | a conversation with one specific agent | `plungeai_execute_agent` with `session_id` ([Agents & missions](/mcp/agents-missions)) | | more on one finished run, with its context | `plungeai_followup` ([Runs, results & follow-ups](/mcp/runs-results)) | Try asking: *"Let's design a due-diligence workflow together."* The thread persists, so you can come back to it from Studio or the CLI. When you are happy with the design, say *"build it"*. Send a turn. This runs a model and costs credits (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_chat","arguments":{"user_request":"let us design a due-diligence workflow together","action":"send","message":"Let us design a due-diligence workflow together. What steps would you include?"}}}' ``` List your conversations: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_chat","arguments":{"user_request":"show my conversations","action":"list_sessions"}}}' ``` Read one back (replace `<conversation_id>`): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_chat","arguments":{"user_request":"show me that conversation","action":"history","conversation_id":"<conversation_id>"}}}' ``` ## 10.2 Memory: plungeai_memory Long-term memory is scoped to you. It has two curated stores: | `target` | Holds | Default size budget | |---|---|---| | `memory` (the default) | notes: facts, preferences and decisions worth keeping | 2,200 characters | | `user` | a profile of you | 1,375 characters | The budgets are per-user settings; the numbers above are the defaults. **Who reads it.** Every mission (`plungeai_run_mission`, [Agents & missions](/mcp/agents-missions)) recalls both stores at the start of its run and can write to them. This is the same mechanism Studio's harness uses, so a note saved here is known to every later run. The recall is a snapshot taken once per run: a write during a run is saved, but it does not change that run's prompt. **What it is not.** Memory is not your run history. There are three separate things: | Store | What is in it | Read with | |---|---|---| | memory | the curated notes and profile above | `recall`, `remember` with `operation: "read"` | | run journal | the goal and full result of each run the harness agent finished | `search_runs`, `get_run` | | execution history | every execution on your account | `plungeai_executions` ([9.4](/mcp/runs-results#94-run-history-plungeai_executions)) | `recall` deliberately leaves the run journal out. **Actions.** | `action` | Needs | Does | Answer | |---|---|---|---| | `recall` | nothing | returns both stores, memory first, then the user profile | the entries, or `No memory recorded yet.` | | `remember` | depends on `operation` (below) | writes the store that `recall` reads | ``Saved to memory (<usage>). recall will see it immediately.`` or `Saved to your user profile (<usage>)…` | | `search_runs` | `query`; optional `limit` (default 10) | keyword search over the run journal | `# Matching runs` and one line per run: `` `<run id>` — <goal> `` | | `get_run` | `run_id` | one journal entry in full | the stored goal and result, or `Run not found.` | | `forget` | exactly one of `entry_id` or `content` | `entry_id` (`mem_…`, the id `GET /v1/memory` lists) removes that one entry from its store; `content` removes every entry containing it from both stores. The same body as One API `POST /v1/memory/forget` | ``Forgot <n> memory entries.`` (`entry` for one). An id your stores do not hold is `Memory entry not found.`; both or neither is `forget takes exactly one of entry_id or content.` (both `isError: true`) | `recall` has no query: it returns all of both stores and your client picks what is relevant. **`remember` operations** (`operation` defaults to `add`, `target` to `memory`): | `operation` | Needs | Effect | |---|---|---| | `add` | `content` | appends an entry. An identical entry is not added twice | | `replace` | `old_text` and `content` | replaces the one entry that contains `old_text` | | `remove` | `old_text` | removes the one entry that contains `old_text` | | `read` | nothing | lists the entries as `# Memory (<target>)` with the store's usage | **When a write is refused** (all `isError: true`): | Answer | Cause | What to do | |---|---|---| | `content is required for remember (what to store; old_text identifies the entry for replace/remove).` | a write with neither `content` nor `old_text` | pass them | | `replace requires old_text and content` / `remove requires old_text` | a half-specified change | pass both | | `no entry matched '<text>'` | `old_text` is in no entry | read the store first | | `'<text>' matches N entries — use a more specific substring. …` | `old_text` is in several entries | use a longer substring | | `Memory at <n>/<limit> chars. … Consolidate now: …` followed by **Current entries** | the write would exceed the budget | `replace` overlapping entries with shorter ones or `remove` stale ones, then retry | | `blocked: content matched threat pattern '<name>'` | the content looks like an instruction-injection attempt | rephrase it as a plain fact | | `<target> store failed the round-trip integrity check (externally modified or oversized entry). Current bytes backed up to <path>. …` (or `entry-size`) | the stored file was changed outside the memory tool; its bytes were backed up | repair the file with the raw memory editor, then retry | **`search_runs`** matches the words of your query that are longer than two characters against the goals in the journal, and ranks runs by how many words match. If no word matches, it returns your most recent runs instead of nothing. `No matching runs.` means the journal is empty. `query is required for search_runs.` and `run_id is required for get_run.` are `isError: true`. Try asking: - *"What do you remember about Circle's acquisitions?"* → `recall` - *"Remember that our fiscal year ends in June."* → `remember` - *"Find my past runs about Tesla."* → `search_runs`, then *"Show me that run."* → `get_run` - *"Forget my old office address."* → `forget` with `content` Recall (read-only): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_memory","arguments":{"user_request":"what do you remember about Circle acquisitions?","action":"recall"}}}' ``` Read the raw memory store (read-only): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_memory","arguments":{"user_request":"show my memory entries","action":"remember","operation":"read","target":"memory"}}}' ``` Save a note. This writes to your memory (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_memory","arguments":{"user_request":"remember that our fiscal year ends in June","action":"remember","content":"Our fiscal year ends in June."}}}' ``` Change it later with `"operation":"replace","old_text":"fiscal year","content":"Our fiscal year ends in July."`. Search the run journal (read-only): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_memory","arguments":{"user_request":"find my past runs about Tesla","action":"search_runs","query":"Tesla","limit":5}}}' ``` ## 10.3 Skills: plungeai_learn `plungeai_learn` manages your private skill library. A skill is a reusable piece of know-how, distilled from a source, that you inject into later missions. | `action` | Needs | Does | Answer | |---|---|---|---| | `learn` (the default) | `source`; optional `name`, `mode` | distills the source into a skill with the `learn` pre-built agent | async (the default): `ok` with ``Started distilling `<id>` (async). Poll plungeai_get_workflow_status with this id, then fetch output with plungeai_get_result once completed.`` and the execution footer. Sync: the run's result | | `list` | nothing | your learned skills, up to 50 | `ok`: `N learned skill(s).` and one line per skill: `` `<id>` — <description> ``. Or `No learned skills yet.` | | `forget` | `name` | deletes one of your skills | `ok`: ``Forgot skill `<name>`.`` | **`source`** is a URL, text or Markdown to distill (up to 65,536 characters), or a description of what you just did, such as a workflow you ran. **`name`** is the skill id in kebab-case: picked for you on `learn` when you leave it out, required on `forget` (take it from `list`). **How `learn` runs.** It starts a one-task harness workflow named `learn` whose task points at the `learn` pre-built agent card. The card is resolved live when the run starts, so an edit to the card changes every later call without a redeploy. The run is a full agent loop (fetch, summarize, save), so it defaults to `mode:"async"`: poll `plungeai_get_workflow_status` with the execution id, then read the result with `plungeai_get_result` ([Runs, results & follow-ups](/mcp/runs-results)). It creates an execution in your history and runs a model. **Where skills go.** A learned skill is a registry skill card tagged with your user id as its owner. Only you can list it or delete it. **Using a skill.** Pass its id in `skills` on `plungeai_run_mission` to inject it into the mission ([Agents & missions](/mcp/agents-missions)). **Answers.** Every answer is the structured outcome envelope ([Tools](/mcp/tools)), because the tool declares an `outputSchema`: | `status` | Summary | Cause | |---|---|---| | `needs_input` | `` `plungeai_learn` was not called — its arguments do not match the tool's schema. `source`: `source` is required to learn a skill. `` | `learn` without `source`; the gateway checks this before the tool runs | | `needs_input` | `` `forget` needs `name` (the skill id from `action: list`). `` | `forget` without `name` | | `error` | ``No skill named `<name>`. Use `action: list` to see yours.`` | no such skill | | `unavailable` | ``Not yours to delete: `<name>`.`` | the skill belongs to someone else | | `error` | `Registry refused the delete (<status>).` | the registry failed the delete | Try asking: - *"Learn a skill from https://developers.cloudflare.com/durable-objects/."* - *"Turn what we just did into a skill called weekly-brief."* - *"What skills have I learned?"* - *"Forget the weekly-brief skill."* List your skills (read-only): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_learn","arguments":{"user_request":"what skills have I learned?","action":"list"}}}' ``` Learn one. This starts an agent run and costs credits (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_learn","arguments":{"user_request":"learn a skill from the Durable Objects docs","action":"learn","source":"https://developers.cloudflare.com/durable-objects/","name":"durable-objects-basics"}}}' ``` ## 10.4 Bot chats: bindings, inbox and groups A saved bot is talked to outside these tools too. Three kinds of conversation around a bot are read and managed with `plungeai_workflow` (the full action table is [8.6](/mcp/workflows#86-manage-saved-workflows--plungeai_workflow)): | Conversation | `action` | What it is | |---|---|---| | Paired chats | `bindings`, `bind`, `unbind` | the Telegram, WhatsApp, Discord or Teams chats bound to the bot, so that chat talks to it. Pair the chat first; `bindings` answers a plain "not enabled yet" until the message gateway supports it | | Bot-to-bot messages | `inbox` | what other bots queued for this bot's next run | | A bot group | `groups`, `group_set`, `group_delete`, `group_thread` | a named set of 2 to 6 of your bots and the thread they share | Try asking: - *"Which chats are bound to my Outreach bot?"* The client runs `bindings`. - *"Show me what the Sales group has been saying."* The client runs `group_thread` with `group: "Sales"`. Reading a group's shared thread is read-only (not run by the docs verifier; it needs a group you made): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_workflow","arguments":{"user_request":"show me what the Sales group has been saying","action":"group_thread","group":"Sales"}}}' ``` # Scheduling Source: https://www.plungeai.com/docs/mcp/scheduling Source: `orchestration/mcp-gateway/extras.ts` (`ScheduleSchema`, `plungeaiSchedule`, `ownedJob`), `orchestration/mcp-gateway/tools.ts` (scheduler ids in `getResult` / `getWorkflowStatus`), `orchestration/mcp-gateway/server.ts` (`TOOL_DEFS`), `orchestration/scheduler/scheduler.ts` (`/jobs` routes), `orchestration/scheduler/SchedulerDO.ts` (heartbeat, model pin, local bots, manual runs), `orchestration/scheduler/utils/cronParser.ts`, `orchestration/scheduler/utils/d1Manager.ts`, `orchestration/scheduler/README.md`. `plungeai_schedule` creates and manages cron jobs on the platform scheduler. A job belongs to the user behind your key, and every run of a workflow, agent, query or `mission_ref` job lands in your normal execution history (heartbeats are the exception, [11.3](#113-create-a-job)), where you read it like any other result ([Runs, results & follow-ups](/mcp/runs-results)). The examples use `https://mcp.plungeai.com/v1`. `https://api.plungeai.com/v1/mcp` is the same server. The tool is not marked read-only, but `stats`, `list`, `get` and `runs` only read. Full parameter table: [14.19](/mcp/tool-reference#1419-plungeai_schedule). ## 11.1 How a schedule runs The scheduler checks for due jobs continuously; the shortest cadence is one minute. A job id looks like `job_<timestamp>_<random>`. Everything this tool creates, except heartbeats, runs as a **workflow job**. Agent and query jobs are wrapped into a saved workflow first, so every run goes through the standard engine pipeline and shows up in your history: | You create | What is saved | What each run does | |---|---|---| | `job_type: "workflow"`, `target` = one of your workflow ids | the job, linked to that workflow | runs that workflow | | `job_type: "agent"`, `target` = an agent id, `parameters.prompt` | a new workflow **Scheduled: <name>** with one task (that agent, that prompt), and the job | runs the wrapper workflow | | `job_type: "query"`, `target` = `{"agent": "<id>", "query": "<text>"}` | the same kind of wrapper workflow | runs the wrapper workflow | | `mission_ref` = a pre-built agent card id | a wrapper workflow **Scheduled: <name>** with one harness task pointing at the card | runs the card, resolved live at fire time | | `job_type: "heartbeat"` | the job | checks a condition each tick and acts only when it is met ([11.3](#113-create-a-job)) | Wrapper workflows are ordinary saved workflows: they appear in `plungeai_list_workflows` and in Studio. Deleting a job does not delete its wrapper. A workflow run is handed to a background run and the scheduler moves on; the run finishes on its own and the job's history is completed afterwards. Results can also be delivered to you (`parameters.deliver`, [11.3](#113-create-a-job)). ## 11.2 Actions | `action` | Needs | Answer | |---|---|---| | `stats` | nothing | `# Schedule stats` with `Jobs: N (a active, p paused)` | | `list` | optional `limit` (default 50) | `# Your scheduled jobs (N)` and a table: Name, Type, Schedule, Status, Next run (epoch milliseconds), ID. Or `No scheduled jobs. Create one with action "create" (name, job_type, target, schedule).` | | `get` | `job_id` | `# <name>` and the whole job as JSON | | `create` | see [11.3](#113-create-a-job) | ``Created scheduled job **<name>** (`<job_id>`) — <cron>.`` plus a note naming the wrapper workflow, if one was made | | `update` | `job_id` and the fields to change | ``Updated job `<job_id>`.`` | | `pause` | `job_id` | ``Job `<job_id>` is now paused.`` | | `resume` | `job_id` | ``Job `<job_id>` is now active.`` | | `delete` | `job_id` | ``Deleted scheduled job `<job_id>`. Its run history is preserved — action "runs" with this job id still shows past executions.`` | | `run_now` | `job_id` | ``Triggered job `<job_id>` — execution `<id>` (fetch the output with plungeai_get_result).`` | | `runs` | optional `job_id`, `limit` (default 20) | `# Scheduled runs (N)` and a table: Job, Status, Started, Execution ID, Error | | `wake` | `job_id`, optional `note` | ``Woke job `<job_id>` — it runs at <time>.`` The job's saved workflow runs once through the scheduler's `runAgain`, with `note` as its input; a pending wake is re-timed, never stacked | | `webhook_mint`, `email_mint`, `hooks_mint` | `job_id`; webhook: `prompt_template`, `scheme`, `filters`; email: `allowed_from`; hooks: `url`, `events` | the scheduler's answer as JSON (token, URL or address part, signing secret). **A secret is in this answer once** and never listed again | | `webhook_list`, `email_list`, `hooks_list` | `job_id` | the job's triggers of that kind as JSON (never a secret) | | `webhook_delete`, `email_delete`, `hooks_delete` | `job_id`, `trigger_id` | ``Deleted <kind> trigger `<id>` of job `<job_id>`.`` An id the job does not have is `Trigger not found.` | | `webhook_rotate`, `email_rotate` | `job_id`, `trigger_id` | mints a successor with the old trigger's settings, then revokes the old one (the same body as One API `POST /v1/triggers/{webhook\|email}/{token}/rotate`): ``Rotated <kind> trigger `<id>` of job `<job_id>`: the old one is revoked.`` and the new trigger as JSON, with `"rotated": "<old id>"`. **The new secret is in this answer once.** Mint comes first, so a failure in between leaves two live triggers, never none. Hooks have no rotate: delete and mint again | `stats` and `list` read up to 1,000 of your jobs and leave deleted jobs out. In `list`, agent and query jobs show the type `workflow`, because they were wrapped. ## 11.3 Create a job Every create needs `name` and `schedule`. The rest depends on the kind of job. The tool picks the path in this order: `job_type: "heartbeat"` first, then `mission_ref` (when set, `job_type` and `target` are not needed), then the plain `job_type` + `target` path. **`schedule`** is a five-field cron expression: `minute hour day-of-month month day-of-week`. For a one-time run use `schedule: "@once"` with `run_at`, an ISO-8601 time with a UTC offset (for example `2026-10-06T09:00:00Z`); without `run_at` the call answers `run_at required` and creates nothing. `ends_at` (ISO-8601, on `create` or `update`) stops a recurring job after that time. The webhook and email triggers belong to workflow jobs, and the hooks (outbound, signed run events) to any job you own; the same set is on the One API as `/v1/schedules` and `/v1/triggers`. | Field | Values | |---|---| | minute | 0–59 | | hour | 0–23 | | day of month | 1–31 | | month | 1–12 | | day of week | 0–6, Sunday = 0 | `*` is any value, `,` a list (`1,15`), `-` a range (`1-5`), `/` a step (`*/15`). | Schedule | Cron | |---|---| | every 5 minutes | `*/5 * * * *` | | every hour | `0 * * * *` | | every day at 09:00 | `0 9 * * *` | | weekdays at 09:00 | `0 9 * * 1-5` | | every Monday at 07:00 | `0 7 * * 1` | | the 1st of every month at midnight | `0 0 1 * *` | `@once` fires through `run_at`: the job runs once at that time and is then done. For a run right now, use `run_now`. **`timezone`** is an IANA zone name such as `America/Los_Angeles` or `Europe/Berlin`. The cron is read in that zone. Without it the cron is read in UTC. An invalid zone is refused, never silently treated as UTC. **`description`** is optional free text. **`parameters`** is an object of job extras: | Key | Meaning | |---|---| | `prompt` | the per-run goal or input. Required for agent jobs. For a workflow job it is the run's input (the scheduler reads `initial_input`, then `input`, then `prompt`). For a `mission_ref` job it is the goal, default `Run your mission for today.` | | `inputs` | named inputs for a workflow job | | `deliver` | where results go: an array of `{channel, chat_id?, to?}` with `channel` one of `inapp`, `email`, `slack`, `telegram`, `whatsapp`, `discord`, `teams`. A bare `{channel:"slack"}` sends you a direct message; a bare `{channel:"email"}` mails your registered address; any other email address must be a confirmed recipient. A bare string such as `"slack"` is accepted and stored as `{channel:"slack"}` | ### Workflow jobs `job_type: "workflow"` and `target` = the id of one of your saved workflows (from `plungeai_list_workflows`). - The workflow must be in your account, or the answer is ``No workflow `<id>` found in your account — pass a workflow id from plungeai_list_workflows, or build one with plungeai_build_workflow.`` - The scheduler refuses a workflow that names an agent id that does not exist: `Scheduler rejected the job: Cannot schedule — no such agent: <ids>. …` - **Model pin.** When you create the job, the first `model:` line in the workflow YAML is recorded, at any indentation (a task's `model:` counts). If that first `model:` value changes later, each run is skipped instead of running on the new model. The run is still recorded as `completed` in `runs`, and the notice `⚠️ Skipped this run: the model changed from <old> to <new> since you scheduled it. Re-save the schedule to accept the new model.` goes to the job's `parameters.deliver` targets. With no `deliver` the skip is silent. The `update` action does not re-record the model; create a new job to accept it. A workflow with no `model:` line anywhere is not pinned. - **Local bots.** A workflow of kind `bot` whose YAML says `local: true` runs on your own computer. When your computer is offline at fire time, the run is skipped instead of failing, and a notice (`⏭️ Skipped "<name>" — this bot runs on your computer, which was offline …`) goes to the job's `parameters.deliver` targets, if it has any. ### Agent and query jobs - **Agent:** `job_type: "agent"`, `target` = an active registry agent id, and `parameters.prompt` = what the agent should do each run. Without the prompt: `Agent schedules need a prompt — pass parameters: { "prompt": "what <agent> should do each run" }.` - **Query:** `job_type: "query"`, `target` = a JSON string `{"agent": "<agent-id>", "query": "<what to run>"}`. Both keys are required. The agent is checked when you create the job: an unknown or inactive agent is refused before anything is saved in the scheduler (`No such agent: …` / `Not permitted: … is not status:active …`). Take agent ids from `plungeai_list_agents` ([Discovery](/mcp/discovery)). ### Pre-built agent cards (`mission_ref`) Pass `mission_ref` with the card id, plus `name` and `schedule`. Each run executes the card as a one-task harness workflow with `parameters.prompt` as its goal. The card is resolved live at fire time, so edits to the card apply to future runs automatically. ### Heartbeats A heartbeat is a proactive check, such as "check my inbox for urgent email every 30 minutes". It needs `job_type: "heartbeat"`, `name`, `schedule`, `check_agent` (or `check_workflow`) and `condition_prompt`; `trigger_workflow`, `notify_channel` and `notify_chat_id` are optional. The tool builds the job's target from these fields. Each tick: 1. `check_agent` is asked `condition_prompt` and told to answer only YES or NO. With `check_workflow` instead, that saved workflow runs as you with `condition_prompt` as its input and its last task must answer YES or NO. 2. On NO, nothing else happens. 3. On YES, `trigger_workflow` (a workflow id) runs with `condition_prompt` as its input, if set. 4. Then a notification goes to `notify_channel` / `notify_chat_id`, if both are set. A heartbeat's check and its `trigger_workflow` run directly on the engine as system runs and create no execution in your history. `run_now` and `runs` show only the scheduler's own `exec_…` id, which the result tools cannot open, and the triggered workflow's output does not appear in `plungeai_executions`. Set `notify_channel` / `notify_chat_id` to see the outcome. `check_agent` must be an active agent. `check_workflow` must be the id of a workflow you own, with no unknown agent in it. Studio's Gmail trigger is a heartbeat built this way: a Gmail search task, then a YES/NO judge. `notify_channel` is one of `telegram`, `whatsapp`, `discord`, `slack`, `teams`, `email`. WhatsApp notifications are **partial**: the WhatsApp adapter needs a phone-number id a heartbeat does not carry, so a WhatsApp heartbeat sends nothing today. ### Create errors All `isError: true`: | Answer | Cause | |---|---| | `create requires: name, job_type (agent\|query\|workflow), target, schedule (cron).` | a plain create with a field missing | | `heartbeat create requires: name, schedule (cron), check_agent or check_workflow …, condition_prompt. …` | a heartbeat with a field missing | | `mission_ref create requires: name, schedule (cron). …` | a `mission_ref` create with a field missing | | `Scheduler rejected the job: Failed to parse cron expression: …` | not a valid five-field cron | | `Scheduler rejected the job: Invalid timezone: <zone>` | not an IANA zone | | `For job_type "query", target must be JSON: {"agent": "<agent-id>", "query": "<what to run>"}.` / `Query target must include both "agent" and "query".` | a malformed query target | | `The scheduler hit a temporary storage error while saving the job — please retry; it usually succeeds on the second attempt.` | a transient storage error | Heartbeat rejections read `Scheduler rejected the heartbeat: …`. **The wrapper is saved first.** For agent, query and `mission_ref` jobs, the **Scheduled: <name>** workflow is saved before the scheduler validates the job. If the scheduler then rejects it (a bad cron, a bad zone), the wrapper stays in your workflows. Delete it with `plungeai_workflow {action:"delete"}` ([Workflows](/mcp/workflows)). Try asking: - *"Run the S&P 500 weekly performance analysis every Monday at 7am."* Your client finds the workflow, then creates a workflow job with `0 7 * * 1` (and your time zone). - *"Every weekday at 9, have the news agent brief me on AI chip news and send it to Slack."* Create a workflow job. This saves a job that runs, and costs credits, on every tick (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"run the S&P 500 weekly performance analysis every Monday at 7am","action":"create","name":"S&P 500 weekly","job_type":"workflow","target":"<workflow_id>","schedule":"0 7 * * 1","timezone":"America/New_York","parameters":{"deliver":[{"channel":"email"}]}}}}' ``` Schedule one agent directly (the tool saves the wrapper workflow for you): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"every weekday at 9 brief me on AI chip news","action":"create","name":"AI chip news","job_type":"agent","target":"newsapi-get-articles","schedule":"0 9 * * 1-5","parameters":{"prompt":"Top AI chip news of the last 24 hours, five bullets with links.","deliver":["slack"]}}}}' ``` ## 11.4 Update, pause, resume, delete `job_id` must be one of your jobs; anything else answers `Job not found.` Passing an execution id by mistake (`exec_…`) answers `That looks like an execution id (exec_…), not a job id (job_…). …`. **`update`** changes only what you pass: `name`, `description`, `schedule`, `timezone`, `target`, `parameters`. `parameters` replaces the stored object, so send the whole object. Other keys (`job_type`, `mission_ref`, the heartbeat fields) are ignored on update. - A workflow job runs the workflow it was linked to at create. A new `target` does not change that link. To schedule a different workflow, create a new job and delete the old one. - A rejected update answers `Scheduler update failed (HTTP <status>).` without the reason. Check the cron and the zone: an invalid cron comes back as HTTP 500, an invalid zone as HTTP 400. **`pause`** and **`resume`** set the job to `paused` or `active`. **`delete` is soft.** The job stops and is marked deleted; its run history stays readable with `runs` and the job id. A deleted job refuses `update`, `pause` and `resume` (`This job is deleted — it cannot be modified. …`) and `run_now` (`This job is deleted — it cannot be run. …`). Deleting it again answers ``Job `<job_id>` is already deleted.`` Deleted jobs drop out of `list` and `stats`. The wrapper workflow of an agent, query or `mission_ref` job is not deleted. **Busy storage.** When the scheduler's storage is briefly throttled, the job lookup is retried once. If it still fails, the answer is `The scheduler’s storage is briefly busy (D1 throttling) — the job most likely still exists. Retry in a few seconds.`, never "Job not found". Try asking: *"What do I have scheduled?"*, *"Pause the S&P 500 job."*, *"Move it to 8am."*, *"Delete that schedule."* Pause a job. This changes its state (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"pause the S&P 500 job","action":"pause","job_id":"<job_id>"}}}' ``` ## 11.5 Run now and run history **`run_now`** runs a job immediately, outside its cadence, and reports the scheduler's real outcome, not just "triggered". - Success: ``Triggered job `<job_id>` — execution `<id>` (fetch the output with plungeai_get_result).`` A workflow run is handed to a background run, so this answer comes back once the run has started, not when it has finished. Poll `plungeai_get_workflow_status` with that id, then read it with `plungeai_get_result`. - When the scheduler has only its own run id: ``Triggered job `<job_id>` — scheduler run `exec_…` (see action "runs").`` - Failure: ``Run failed (scheduler run `exec_…`): <reason>`` (`isError: true`). **`runs`** lists run history: - with `job_id`: that job's runs, including the runs of a deleted job; - without `job_id`: the runs of your current jobs. Runs of deleted jobs are left out; ask for them by job id. `limit` defaults to 20 (1–100). The columns are Job, Status, Started, Execution ID and Error (first 120 characters). The **Execution ID** is the id `plungeai_get_result` and `plungeai_get_workflow_status` accept: the platform execution id, or the engine's `exec-…` id. Heartbeat runs, and only very old runs of other jobs, fall back to the scheduler's own `exec_…` id, which those tools redirect back here. `No executions for your jobs yet.` means there are none. Try asking: *"Run it now."*, *"Show me the run history of that job."* Run history (read-only): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"show me the run history of that job","action":"runs","job_id":"<job_id>","limit":10}}}' ``` Your schedule at a glance (read-only): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"what do I have scheduled?","action":"stats"}}}' ``` Run a job now. This runs the job and costs credits (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"run it now","action":"run_now","job_id":"<job_id>"}}}' ``` # Prompts & resources Source: https://www.plungeai.com/docs/mcp/prompts-resources Source: `orchestration/mcp-gateway/prompts.ts` (`handlePromptList`, `handlePromptGet`), `orchestration/mcp-gateway/resources.ts` (`handleResourceList`, `handleResourceRead`), `orchestration/mcp-gateway/server.ts` (resource templates, handler wiring), `orchestration/mcp-gateway/skill-dist.generated.ts`, `orchestration/mcp-gateway/llms-routes.ts`, MCP SDK 1.29.0 (`types.js` `McpError`, `shared/protocol.js`). Besides its 22 tools, the server offers **one prompt** and **five resources** plus **three resource templates**. They are read-only and run nothing, and they work on `https://mcp.plungeai.com/v1` and on the same server at `https://api.plungeai.com/v1/mcp`. All content is live: registry listings come from the registry at request time, and workflow and execution resources are read from your account with an ownership check. There is no static manifest. ## 12.1 The `/plungeai` prompt There is exactly one prompt. It activates the whole platform in one step. Earlier recipe prompts (research pipeline, market research, …) were removed on purpose: they showed up as separate slash commands and locked the client into one behaviour, when the tools already cover everything (`prompts.ts`). | Field | Value | |---|---| | `name` | `plungeai` | | `title` | `PlungeAI` | | `description` | `PlungeAI MCP` | | `arguments` | one: `request` (string, **optional**), "What you want to accomplish (optional)" | **Using it in a client.** Clients list MCP prompts in their slash-command or prompt menu. In Claude Code the prompt appears as `/plungeai:plungeai (MCP)`: type `/plungeai` to filter the menu and pick it, or run `/mcp__plungeai__plungeai` directly (the middle part is the server name you chose in `claude mcp add`). Claude Code splits any text after the command on whitespace, one word per argument, so only the first word would reach `request`. Run the prompt bare, then type your request as the next message. Other clients may offer a form field for `request` instead. - run bare (no `request`): the model is told to ask you what you want to accomplish; - with `request` set (for example `research Tesla's Q2`, from a client that offers the field): the model is told to handle that request now. The prompt is a convenience, not a requirement. You can always just talk to your client normally. `prompts/list`: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"prompts/list"}' ``` `prompts/get` returns `description: "PlungeAI MCP"` and one `user` message (type `text`). It runs nothing, so it is free: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"prompts/get","params":{"name":"plungeai","arguments":{"request":"research Tesla'"'"'s Q2"}}}' ``` What the message tells the model: - It now has the full platform through the `plungeai_*` tools: about 90 active agents, CNL workflows, missions, long-term memory and execution history. - **Discover.** "Show me my agents/workflows" means saved workflows (`plungeai_list_workflows`). The registry (`plungeai_list_agents` with a natural-language `search`) is for building blocks. Fetch a full card with `agent_id` before first use. - **Execute.** `plungeai_execute_agent` (one agent, optional `session_id`), `plungeai_execute_workflow` (saved `workflow_id` or ad-hoc CNL YAML), `plungeai_run_mission` (bounded autonomous loop with memory). - **Results.** `plungeai_get_result`, `plungeai_get_workflow_status` (poll async runs), `plungeai_executions`, and `plungeai_followup` / `plungeai_continue` for finished or paused runs. - **Approvals.** Relay any "⏸ AWAITING USER APPROVAL" or "⏸ AWAITING USER" block verbatim, never approve on its own, then call `plungeai_continue` with `approve: true` or the user's words. - **Build.** `plungeai_build_workflow` generates and saves a workflow. `plungeai_workflow` is direct CRUD. `plungeai_memory` is long-term memory. - Always pass `user_request`, the user's words verbatim. - The last line is either `Now handle this request: <request>` (the argument is trimmed) or `Ask the user what they want to accomplish.` when `request` is empty or absent. An unknown prompt name is **rejected** with `-32602`. The SDK prefixes the message with `MCP error -32602:`: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"research"}}' ``` ## 12.2 Resources at a glance `resources/list` returns five fixed resources (`handleResourceList`): | URI | `name` | `mimeType` | Returns | Scope | |---|---|---|---|---| | `plungeai://agents/list` | PlungeAI Agents | `text/markdown` | the active registry agents, up to 100, as registry Markdown | platform (active agents only) | | `plungeai://agents/categories` | Agent Categories | `text/markdown` | a category → agent-count table | platform (active agents only) | | `plungeai://personas/list` | Personas | `text/markdown` | persona cards, up to 100, as registry Markdown | platform | | `plungeai://workflows/list` | Your Workflows | `text/markdown` | your 50 most recently updated saved workflows | **your account** | | `plungeai://docs/workflow-authoring` | Workflow Authoring Guide | `text/markdown` | the full CNL workflow-authoring skill bundle | static | `resources/templates/list` returns three URI templates (`server.ts`) for resources addressed by a parameter: | `uriTemplate` | `name` | `mimeType` | Scope | |---|---|---|---| | `plungeai://agents/{category}` | Agents by category | `text/markdown` | platform (active agents only) | | `plungeai://workflows/{id}` | Workflow YAML by id | `text/yaml` | **your account**, ownership-checked | | `plungeai://executions/{id}` | Execution result by id | `text/markdown` | **your account**, ownership-checked | The resources and templates carry no `size` or `annotations`. The templates carry no `description`. Every `resources/read` answers `{ "contents": [ { "uri", "mimeType", "text" } ] }` with one entry. The server's `resources` capability is `{}`: no `subscribe` and no `listChanged` ([Protocol & transport](/mcp/protocol)). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":4,"method":"resources/list"}' ``` ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":5,"method":"resources/templates/list"}' ``` **Resources or tools?** A resource is the same data some tools return, in a form your client can attach to a conversation without a tool call. For searching and filtering, use the tools: `plungeai_list_agents` for a semantic search, `plungeai_list_workflows` for folders and search, `plungeai_get_result` for one task's output ([Discovery](/mcp/discovery), [Runs, results & follow-ups](/mcp/runs-results)). ## 12.3 `plungeai://agents/list` The live registry of **active** agents: `kind=agents&status=active&format=md&limit=100`. The Markdown starts `# Registry search`, then `*<n> results*`. Each agent follows under a `## Name (id)` heading with its kind, category and status line, its description and a **Good at** list. On 2026-09-24 the registry listed **92** active agents (live-verified). The listing is large, about 150 KB, so attach it only when you really want the whole catalog. For a normal question, a filtered `plungeai_list_agents` search is far smaller. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":6,"method":"resources/read","params":{"uri":"plungeai://agents/list"}}' \ | cut -c1-1200 ``` ## 12.4 `plungeai://agents/categories` Active agents (up to 500) grouped by category into one table, largest first. An agent with no category counts as `general`: ``` # Agent Categories (<n> agents, live registry) | Category | Agents | |:---|---:| | google-workspace | … | … _Read `plungeai://agents/{category}` for the agents in one category._ ``` This is the cheap way to answer "what kinds of agents are there". On 2026-09-24 the registry held 92 active agents in 14 categories (live-verified): `google-workspace`, `tools`, `search`, `microsoft-365`, `ai`, `blockchain`, `communication`, `ecommerce-agents`, `payment`, `entertainment`, `media`, `financials`, `shopping`, `coding-agents`. Note the finance category is `financials`, not `finance`. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":7,"method":"resources/read","params":{"uri":"plungeai://agents/categories"}}' ``` ## 12.5 `plungeai://personas/list` The persona registry: `kind=personas&format=md&limit=100`, as registry Markdown (a `## Name (id)` heading per persona, its kind and category, **Good at**). Unlike the agent resources, it has **no** `status:active` filter, because no persona card carries that tag and the filter would empty the list (`resources.ts`). The list stops at 100: on 2026-09-24 the registry answered `*100 results*`. For a specific persona, search with `plungeai_list_agents` `{"kind":"personas","search":"…"}`. A persona id goes on `plungeai_execute_agent` `persona` or a mission's `persona` ([Agents & missions](/mcp/agents-missions)). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":8,"method":"resources/read","params":{"uri":"plungeai://personas/list"}}' \ | cut -c1-800 ``` ## 12.6 `plungeai://workflows/list` **Your workflows**: the 50 most recently updated saved workflows on your account (what users call "my agents"). One bullet each (bold name, the id as code, then the description) under `# Your Workflows (<n>)`. With no saved workflows, the text is `_No workflows yet._`. It carries no folders and no search. For those, use `plungeai_list_workflows`. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":9,"method":"resources/read","params":{"uri":"plungeai://workflows/list"}}' ``` ## 12.7 `plungeai://docs/workflow-authoring` The complete CNL workflow-authoring skill as one Markdown document: how to author, validate and save CNL workflows, with examples. It is a static bundle built into the server and needs no data access. The content is identical to `GET https://mcp.plungeai.com/llms-full.txt`, which needs no key (61,309 bytes on 2026-09-24, live-verified). Attach it when you want your client to write CNL YAML by hand ([Workflows](/mcp/workflows)). The same skill is in the catalog at `https://skills.plungeai.com`. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":10,"method":"resources/read","params":{"uri":"plungeai://docs/workflow-authoring"}}' \ | cut -c1-600 ``` ## 12.8 `plungeai://agents/{category}` The active agents in one category, up to 100, as registry Markdown under `# Registry search — category=<category>`. The category is passed to the registry as-is, so an unknown or misspelled category is **not** an error. It returns an empty listing (`*0 results*`). Take category names from `plungeai://agents/categories`. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":11,"method":"resources/read","params":{"uri":"plungeai://agents/financials"}}' ``` ## 12.9 `plungeai://workflows/{id}` The saved CNL YAML of one of **your** workflows, as `text/yaml`. The id is the workflow id from `plungeai_list_workflows` or `plungeai://workflows/list`. The ownership check comes first: an id that does not exist and an id owned by someone else get the same `-32002` answer, so you cannot probe for other accounts' ids. A workflow whose YAML body is missing from storage returns the text `No YAML content found`. ```bash # needs one of your workflow ids — not auto-verified curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":12,"method":"resources/read","params":{"uri":"plungeai://workflows/<workflow id>"}}' ``` ## 12.10 `plungeai://executions/{id}` The stored result of one of **your** runs, by **execution id** (not the workflow id, [Troubleshooting](/mcp/troubleshooting)). It reads the same stores as `plungeai_get_result`: the stored result, then the final task's output. The document looks like this: ``` # Execution <execution id> - **Workflow:** `<workflow id>` ## Result <the run's final output> ``` When there is no output, the Result section says why: | Run state | `## Result` text | |---|---| | failed | `_This run failed: <error message>._` (or `no error was recorded`) | | no output yet | `_No result stored yet (may still be running)._` | This resource gives the final result only. For one task's output, the full conversation thread, the "Steps in this run" index or a paused run's ⏸ block, use `plungeai_get_result` ([Runs, results & follow-ups](/mcp/runs-results)). An unknown id, or one that is not yours, gets `-32002 Execution not found: <id>`. ```bash # needs one of your execution ids — not auto-verified curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":13,"method":"resources/read","params":{"uri":"plungeai://executions/<execution id>"}}' ``` ## 12.11 Resource errors `resources/read` answers JSON-RPC errors, not outcomes. The SDK prefixes `McpError` messages with `MCP error <code>:` (`types.js`). | Case | `code` | `message` | |---|---|---| | URI matches no resource or template | `-32002` | `MCP error -32002: Resource not found: <uri>` | | `plungeai://workflows/{id}`: unknown or not yours | `-32002` | `MCP error -32002: Workflow not found: <id>` | | `plungeai://executions/{id}`: unknown or not yours | `-32002` | `MCP error -32002: Execution not found: <id>` | | The registry answered non-2xx (agent, category and persona resources) | `-32603` | `Registry error <status>` | | `params.uri` missing or not a string | `-32603` | the SDK's raw schema-validation text (a zod issue list) | ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":14,"method":"resources/read","params":{"uri":"plungeai://nope"}}' ``` Registry-backed reads time out after 10 seconds. For the full error vocabulary, see [Errors, feedback & limits](/mcp/errors-limits). # Errors, feedback & limits Source: https://www.plungeai.com/docs/mcp/errors-limits Source: `orchestration/mcp-gateway/tool-outcome.ts`, `server.ts` (`wrap`, `validateToolArgs`), `index.ts`, `shared.ts`, `auth-middleware.ts`, `rate-limiter.ts`, `session-do.ts`, `resources.ts`, `prompts.ts`, `tools.ts`, `tool-exec.ts`, `extras.ts`, `activity-log.ts`, `log-view.ts`, `execution-do.ts`, `leg-state.ts`, `core/core-base/content-guard.ts`, `orchestration/api-gateway/lib/auth.ts`, MCP SDK 1.29.0, live registry cards (2026-09-24). When something cannot run, the platform does not just fail. It answers with **what is missing and how to fix it**. Your client reads that answer and usually corrects itself. This chapter is so that *you* know what is happening: the answer shapes, every error code, the limits, when to retry, and how to read your own activity log. It applies to `https://mcp.plungeai.com/v1`. The same server at `https://api.plungeai.com/v1/mcp` differs only at the edge ([4.11](/mcp/protocol#411-the-two-endpoints-at-the-transport-level)). ## 13.1 Three kinds of "no" ``` Your request ├─ stopped at the edge: auth, size, rate limit, parse, → JSON-RPC error + HTTP status (13.6) │ unknown or fenced tool, unknown method/resource/prompt nothing ran ├─ reached a tool that answered with guidance → a result with an outcome (13.2) │ needs_input / needs_connection / needs_api_key / usually nothing ran │ needs_approval / unavailable / error └─ reached a tool that answered with a plain failure → a result, isError: true, text only (13.5) ``` A client should branch in this order: first the HTTP status and the JSON-RPC `error`, then `result.isError`, then `result.structuredContent.status`. ## 13.2 The structured-feedback contract The six tools whose `outputSchema` is the outcome envelope **never answer with a bare error**: `plungeai_execute_workflow`, `plungeai_execute_agent`, `plungeai_execute_tool`, `plungeai_get_tool_contract`, `plungeai_run_mission` and `plungeai_learn` ([4.9](/mcp/protocol#49-outputschema-and-structuredcontent)). Every answer carries an **outcome**: a `status` plus a `summary`, and usually a `remediation` saying exactly what to do. `plungeai_get_result` uses the same envelope for its guidance answers (a missing or unknown id). A paused run is not an envelope there: its result text ends with a ⏸ block, and `format:"json"` gives `{"content": …}` with no `status`. For machine-readable pause state, read `structuredContent.continuation` from `plungeai_get_workflow_status` ([9.3](/mcp/runs-results#93-check-status-plungeai_get_workflow_status)). Two paths hold the promise for **every** tool: argument validation ([13.3](#133-argument-validation)) and unexpected exceptions ([13.4](#134-transient-failures)). | `status` | Text starts with | What it means | What to do | `isError` | |---|---|---|---|---| | `ok` | the result itself | It ran. This is your result. | Nothing. | `false` | | `needs_input` | `📝 NEEDS INPUT` | A required field is missing or malformed. The answer names the exact field and, where the agent has one, **includes its full form** (the contract) with a worked example. A run that paused with a question also uses this status (`⏸ AWAITING USER: …`). | Say *"do what it suggests"*. Your client fills the form and retries. For a question, answer it (`plungeai_continue`). | `false` | | `needs_connection` | `🔌 NEEDS CONNECTION` | The agent acts as *you* (Google, Microsoft, …) and your **connected account** is missing, expired, short of scopes, or lacks a licence. Nothing was run: the check comes first. | Connect the service once in Studio → Connectors, then ask again. | `false` | | `needs_api_key` | `🔑 NEEDS API KEY` | A bring-your-own-key agent (wallets, private APIs) has no key saved for you. | Save your key once in Studio. Every surface can then use it. | `false` | | `needs_approval` | `⏸ NEEDS APPROVAL` | Nothing has executed. Either an agent staged an action and paused (the summary starts `⏸ AWAITING USER APPROVAL: …`, with an `approve_via` action), or the **trust fence** refused an outward, irreversible operation (send, pay, transfer, buy, withdraw) before it ran (no actions, no paused session). | Staged action: reply "yes"/"approve" to proceed (`plungeai_continue`), or anything else to change course. Trust-fence refusal: nothing to continue over MCP; the operation runs only from Ocean Studio, where a person approves it ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)). | `false` | | `unavailable` | `🚫 UNAVAILABLE` | The agent cannot run here. Its id does not exist (`No such agent: …`), it is not `status:active`, it is registered but not deployed, or it reported itself temporarily unable (for example its data provider's quota ran out). Apart from a made-up id, this is a platform-side gap, **not your mistake**. | Pick one of the live alternatives listed in the answer, or retry later if it gave a reason. | `false` | | `error` | `❌ ERROR` | A genuine failure, with the real reason stated (for example an upstream API limit, or a timeout). | Fix the input and rerun, or follow the retry hint. Don't keep polling. | **`true`** | Every non-`ok` answer ends with the line `_This is structured guidance, not a crash — do not retry the identical call. Follow the remediation, or relay it to the user._` **The envelope** is the `structuredContent` object: `status`, `summary`, `agent_id`, `operation`, `result`, `execution_id`, `missing[]`, `warnings[]`, `remediation { message, actions[] }` and `contract` (field reference in [4.9](/mcp/protocol#49-outputschema-and-structuredcontent)). The Markdown text shows the same information as a status line, the summary, `Missing required: …`, `**To proceed:** <message>` and one bullet per action. Each warning becomes a `⚠️` line. **Remediation actions** (`RemediationAction`), and how each is rendered for your client: | `action` | Fields | Rendered as | |---|---|---| | `provide_field` | `field`, `description?` | Provide `<field>` — `<description>` and retry the same call. | | `connect_provider` | `provider`, `url`, `scopes?` | Connect **provider**: open `<url>` → Connectors and connect the account. Relay this to the user — it needs their browser. | | `provide_api_key` | `provider`, `how` | Provide a **provider** API key: `<how>` | | `approve_via` | `tool: plungeai_continue`, `execution_id`, `note?` | Ask the user to approve, then call `plungeai_continue {execution_id, approve: true}`. NEVER approve on your own. | | `respond_via` | `tool: plungeai_continue`, `execution_id`, `question?` | Relay the question to the user, then call `plungeai_continue {execution_id, message: "<their answer>"}`. | | `retry_with` | `changes`, `note?` | Retry with `{…}` (for example `{"mode":"async"}` after a timeout). | | `use_alternative` | `agent_id`, `note?` | Alternative: `<agent_id>` — `<note>` | Connection and key remediations point at Ocean Studio, `https://studio.plungeai.com` → Connectors. **A refusal is not a run.** If the platform stops a call before dispatch (a missing field, a missing connection, an unknown or inactive agent, an unknown model), no execution is created. Nothing half-happens, and your history stays clean. Looking up a result by an id that does not exist gets a `needs_input` answer naming the id (`plungeai_get_result`), never a bare "not found". **Limits are told to you, never silently applied.** Search engines declare their maximum query length on their card (live-verified 2026-09-24): `serper-agent` 2,048 characters, `brave-agent` 400, `tavily-agent` 400. Send more and you get `needs_input` with the limit. **Your input is never truncated behind your back.** **The real reason comes back.** When a run fails, the classifier (`classifyExecutionError`) passes the upstream reason through: `Execution failed: <reason>`, `The run timed out: <reason>`, or the agent's own message on `unavailable`. You do not get a generic "something went wrong". **Visible mapping, never guessing.** Send only prose to `plungeai_execute_tool` for an agent whose form has exactly **one** required free-text field (a report generator's `content`, say), and the platform fills that field with your prose and says so: `` ⚠️ prose mapped: your prompt was passed as `content` (this card's only required parameter)… ``. Details are in [Tools](/mcp/tools). ## 13.3 Argument validation Every `tools/call` is checked against the tool's input schema **before** the tool runs (`server.ts` `validateToolArgs`, called from `index.ts`). A failure is not a protocol error. It is a `needs_input` outcome, returned as a normal JSON-RPC result (HTTP 200, plain JSON): - `summary`: `` `plungeai_list_agents` was not called — its arguments do not match the tool's schema. `` followed by one `` `field`: message `` per problem; - `missing[]`: required fields that were absent; - `remediation.actions`: one `provide_field` per missing or bad field. Nothing runs, but the call still counts toward your rate limit. Free to try: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_list_agents","arguments":{"user_request":"list agents","limit":500}}}' ``` The bounds every tool shares (`shared.ts`, `tools.ts`). Per-tool parameters are in the [Tool reference](/mcp/tool-reference). | Kind of argument | Bound | |---|---| | ids (`agent_id`, `execution_id`, `workflow_id`, `task_id`, `model`, …) | string, at most 256 characters | | short text (`search`, `folder`, `description`, `query`, …) | at most 2,000 characters | | prompts and messages (`prompt`, `input`, `goal`, `message`, `content`, `source`, …) | at most 65,536 characters | | `workflow_yaml`, `yaml` | at most 262,144 characters (256 KiB) | | `user_request` | at most 4,000 characters | | `limit` | integer 1–100 | | `inputs` (`plungeai_execute_workflow`) | keys up to 128 characters, values up to 8,192 | | `max_iterations` (`plungeai_run_mission`) | integer 1–50 | | `maxTokens` / `max_tokens` (`plungeai_execute_agent`) | integer 1–64,000 | This pre-screen covers **single-message** requests. Inside a JSON-RPC batch, bad arguments come back as the SDK's own `isError` text (`Input validation error: …`) instead ([4.4](/mcp/protocol#44-json-rpc-methods)). ## 13.4 Transient failures If a tool throws unexpectedly (a database hiccup, a service-binding blip), the server still answers with an outcome (`server.ts` `wrap`): - `status: "error"`, `isError: true`; - `summary`: the error message and `(request <request id>)`. Messages that would expose internals (database or parser errors) are replaced with `An internal error occurred while running this tool.`; - `remediation.message`: `This is a transient platform failure, not a bad request. Retry once in a few seconds; if it persists, report the request id above.` `format: "json"` is honoured on this path too. The full error goes to the server logs under the same request id. ## 13.5 Plain tool errors Tools without an `outputSchema` answer their own failures as a plain text result with `isError: true` and no `structuredContent`. Examples from the code: | Tool | Text | |---|---| | any tool without a user (internal calls only) | `Authentication required.` | | `plungeai_get_workflow_status` | `Execution not found.` (the id is unknown or not yours) | | `plungeai_get_result`, `plungeai_get_workflow_status` | `That is a scheduler run id, which this tool cannot resolve. Use plungeai_schedule {action: "runs"} …` (ids starting `exec_`) | | `plungeai_executions` | `execution_id is required for this action.` · `Execution not found.` · `Execution is <status>, not running — nothing to cancel.` | | `plungeai_list_agents` | `Registry unavailable — try again shortly.` · `Registry search failed (<status>).` | They are short and say what to do. Treat them like a `needs_input` or `error` outcome. ## 13.6 JSON-RPC and HTTP errors Protocol errors mean the request never reached a tool. The body is always `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. `id` is `null` when the server had not parsed one. | HTTP | `code` | `message` | When | |---|---|---|---| | 400 | `-32700` | `Parse error` | the body is not valid JSON | | 400 | `-32700` | `Parse error: Invalid JSON-RPC message` | valid JSON, but not a JSON-RPC message (SDK) | | 400 | `-32600` | `Invalid Request: Only one initialization request is allowed` | a batch holding `initialize` plus other messages | | 400 | `-32600` | `Invalid Request: Server already initialized` | a second `initialize` on an opt-in session | | 400 | `-32000` | `Bad Request: Unsupported protocol version: <v> (supported versions: …)` | an unsupported `MCP-Protocol-Version` header on a request after `initialize` (not checked on `initialize`, nor on a `tools/list` served from cache, [4.3](/mcp/protocol#43-protocol-version-and-initialize)) | | 401 | `-32001` | `No valid authentication provided. Use an X-API-Key header or "Authorization: Bearer <api key>".` | no key, or the key is wrong, revoked or expired, or a Bearer token that is not `ozk_`. `WWW-Authenticate: Bearer` when you sent no credentials, `Bearer error="invalid_token"` when you did. | | 401 | `-32001` | `API key not permitted from <ip>` | the key is fenced to certain IP addresses and this is not one | | 403 | `-32000` | `Invalid Host header: <host>` | the request reached the server under a hostname it does not serve | | 404 | `-32001` | `Session not found` | an `Mcp-Session-Id` that is unknown, expired, belongs to another key, or is over 128 characters ([4.5](/mcp/protocol#45-stateless-by-default-opt-in-sessions)) | | 405 | `-32000` | `Method not allowed.` | `GET` or `DELETE` without a session. Header `Allow: POST, OPTIONS`. | | 413 | `-32600` | `Request body too large (1 MiB max)` | over 1,048,576 bytes ([13.8](#138-body-caps-and-timeouts)) | | 415 | `-32000` | `Unsupported Media Type: Content-Type must be application/json` | a complete `Accept` header with a non-JSON `Content-Type` | | 429 | `-32000` | `Rate limit exceeded: Rate limit: <n> requests/minute` (or `/day`) | tier limit reached ([13.7](#137-rate-limits)). `Retry-After` header plus `error.data.retryAfter` (seconds). | | 429 | `-32000` | `Too many failed authentication attempts` | too many failed logins from your IP. `Retry-After` header. | | 200 | `-32602` | `Tool not found: <name>` | the tool name does not exist (single-message `tools/call`) | | 200 | `-32602` | `Tool not permitted for this key: <name>` | the tool exists, but your key is fenced to other tools | | 200 | `-32602` | `Invalid params: <fields>` | malformed `initialize` params | | 200 | `-32602` | `MCP error -32602: Prompt not found: <name>` | `prompts/get` with an unknown name | | 200 | `-32002` | `MCP error -32002: Resource not found: <uri>` (also `Workflow not found` / `Execution not found`) | `resources/read` ([12.11](/mcp/prompts-resources#1211-resource-errors)) | | 200 | `-32601` | `Method not found` | a method the server does not implement ([4.4](/mcp/protocol#44-json-rpc-methods)) | | 200 | `-32603` | `Registry error <status>`, or the SDK's validation text | a registry failure behind a resource, or request params that fail the SDK schema | | 500 | `-32603` | `Internal error` | an unhandled server error | | 404 | — | `{"error":"Not found. MCP endpoint: POST /v1 (Streamable HTTP)."}` | any path other than `/v1`, `/v1/`, `/mcp` and the public routes (not JSON-RPC) | Unauthenticated requests, live-verified 2026-09-24: ```bash curl -s -i -X POST https://mcp.plungeai.com/v1 -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` ```bash curl -s -i -X POST https://mcp.plungeai.com/v1 -H 'Authorization: Bearer not-a-key' \ -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' ``` An unknown tool is a protocol error, so it cannot be mistaken for "the tool ran and failed": ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"plungeai_nope","arguments":{}}}' ``` On `https://api.plungeai.com/v1/mcp`, the router answers its own edge errors in the One API envelope: `401 unauthorized` (`Bearer ozk_ key required` / `Invalid or revoked API key`), `429 rate_limited`, `413 payload_too_large`, each with `X-Error-Code` ([One API errors](https://api.plungeai.com/docs#13-errors-limits--headers)). Everything the MCP server answers passes through unchanged. ## 13.7 Rate limits Your key's tier sets two fixed windows, per minute and per day (`rate-limiter.ts`): | Tier | Per minute | Per day | |---|---|---| | `free` | 30 | 1,000 | | `pro` | 100 | 10,000 | | `enterprise` | 300 | 100,000 | - **Only tool calls count.** `initialize`, `tools/list`, `resources/*`, `prompts/*` and `ping` are free on `https://mcp.plungeai.com/v1` and on `https://api.plungeai.com/v1/mcp`, which forwards your key to the same gateway ([4.11](/mcp/protocol#411-the-two-endpoints-at-the-transport-level)). - **The counter is per account** (your user id), not per key. Two keys on one account share it. - **Windows are fixed, not calendar-aligned.** A window starts at the first counted call after the previous one ended and lasts 60 seconds (or 24 hours). - A key with a missing tier is treated as `free`. An operator can give a key its own per-minute limit, which replaces the tier's per-minute number (the daily limit stays). Self-service keys use the tier limits. - **Failed logins** are counted per client IP: 20 per minute and 500 per day. After that the server answers `429 Too many failed authentication attempts` until the window resets. On a limit hit you get `429` with `Retry-After` (seconds until the window resets, at least 1) and `error.data.retryAfter`, and the call does not run. Ask *"how much of my rate limit have I used?"*: `plungeai_whoami` shows `minute_used` and `day_used` for the current windows. Its own call counts, so the number includes it. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"how much of my rate limit have I used?"}}}' \ | sed -n 's/^data: //p' ``` These limits bound request rates. Spending (model tokens, paid APIs) is metered separately on your account, and money-moving actions stop at the trust fence ([Tools](/mcp/tools)). ## 13.8 Body caps and timeouts | Limit | Value | What happens | |---|---|---| | Request body | 1 MiB (1,048,576 bytes) | `413 -32600`. Checked against `Content-Length` before auth, then enforced on the stream itself (chunked bodies included). | | Argument sizes | [13.3](#133-argument-validation) | `needs_input`, nothing runs | | Keepalive | a progress line every 15 s while a tool runs | keeps the SSE stream from going idle ([4.6](/mcp/protocol#46-toolscall-over-sse-progress-and-keepalive)) | | Registry-backed reads | 10 s | `Registry error` / `Registry unavailable` | | Background runs | legs of 10 min; re-entered after 12 min; failed after 3 kills in a row without a checkpoint; `Interrupted (run did not complete)` after 16 min with no heartbeat | [4.7](/mcp/protocol#47-async-mode) | | Opt-in sessions | 10 min idle | `404 Session not found`; re-initialize | The server itself sets no request timeout on a `tools/call`. Your client may: Claude Desktop cuts a single tool call at about 4 minutes (`server.ts`). Claude Code lets you set a per-server `timeout` (milliseconds) in `.mcp.json`, or `MCP_TOOL_TIMEOUT` ([Claude Code docs](https://code.claude.com/docs/en/mcp), read 2026-09-24). For anything long, use `mode: "async"` ([4.7](/mcp/protocol#47-async-mode)). ## 13.9 Retry guidance | You got | Do | |---|---| | `429` rate limited | Wait `Retry-After` seconds, then retry. Don't hammer: every retry inside the window is refused again. | | `401` | Don't retry. Fix or replace the key ([Authentication](/mcp/authentication)). From a new location, check whether the key is IP-fenced. | | `-32602 Tool not found` | Refresh the tool list: reconnect your client, or call `tools/list` again. | | `-32602 Tool not permitted for this key` | Don't retry. Use a key that allows the tool. | | `404 Session not found` | Drop the `Mcp-Session-Id` and re-initialize. | | `413` | Shrink the request. Move long text into a saved workflow, or split the work. | | outcome `needs_input`, `needs_connection`, `needs_api_key` | Follow the remediation, then retry. **Never** retry the identical call blind. | | outcome `needs_approval` with an `approve_via` action (an agent staged the action and paused) | Ask the user. Only after an explicit yes, call `plungeai_continue` with `approve: true`. | | outcome `needs_approval` with no actions (trust-fence refusal: send / pay / buy / transfer / withdraw …) | Nothing to continue: relay it to the user. The operation runs only from Ocean Studio, where a person approves it ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)). `plungeai_continue` answers `No conversation session…`. | | outcome `unavailable` | Use an alternative from the answer. If the agent gave a temporary reason, retry later. | | outcome `error` "transient platform failure" | Retry **once** after a few seconds. If it repeats, report the request id. | | outcome `error` "The run timed out" | From `plungeai_execute_workflow`, `plungeai_execute_tool` or `plungeai_run_mission`: retry with `mode: "async"` and poll `plungeai_get_workflow_status`. `plungeai_execute_agent` has no `mode` and drops it, so the same retry runs synchronously again: run the agent as a one-task workflow through `plungeai_execute_workflow` with `mode: "async"` instead. | | outcome `error` "Execution failed: …" | Read the reason and fix the input. Don't keep polling a failed run. | | `-32603` / `500` | Retry with backoff. If it persists, report it with the request id from your activity log. | ## 13.10 The activity log The server keeps a readable trail of everything your account did over MCP: one Markdown entry per request, grouped per account per UTC day (`activity-log.ts`, `log-view.ts`). Use it to see exactly what your client sent, which is often **not** what you typed, and what came back. **Read it** with your own key. It is assembled live on every request, newest first: ```bash curl -s "https://mcp.plungeai.com/log?date=$(date -u +%F)" \ -H "Authorization: Bearer $PLUNGE_API_KEY" ``` | Query | Meaning | |---|---| | `date=YYYY-MM-DD` | the UTC day. Defaults to today. Anything else gets `400 {"error":"date must be YYYY-MM-DD"}`. | | `format=md` / `format=html` | force Markdown or HTML. Otherwise a browser (`Accept: text/html`) gets a rendered page, and curl gets `text/markdown`. | | `refresh=1` | also rewrite the stored per-day document now (normally a cron does it every 15 minutes) | | `key=<your key>` | for opening the log in a browser, where you cannot set a header. The key then sits in the URL and your browser history, so prefer the header. | The same handler also answers at `/admin/log`. Responses are `Cache-Control: no-store`. A key reads only **its own account's** trail. A missing or wrong key gets `401 {"error":"Provide your API key: Authorization: Bearer <key>, X-API-Key, or ?key="}`. The log is served on `https://mcp.plungeai.com` only. **What an entry looks like** (the shape `activity-log.ts` renders): ``` ### 14:03:12 · `plungeai_whoami` · ✅ · 212ms - **User:** you@example.com · <your user id> · <client IP> · `<request id>` - **Key:** <key label> > **Asked:** how much of my rate limit have I used? **Q:** - **format:** markdown - **A:** <the answer, whole if 300 characters or fewer, else "<n> chars · <first 240 characters>…"> ``` | Part | What it holds | |---|---| | heading | UTC time, the event (a tool name, or `initialize`, `tools/list`, `resources/read`, …, or `rate_limited`, `tool_denied`), ✅/❌ for tool calls, duration | | **User** / **Key** | the account, email, client IP and request id, and which key was used (its label). One account can hold several keys. Calls through `https://api.plungeai.com/v1/mcp` show your key label like any other call. | | **Asked** | your `user_request`, in your own words, when the client sent one. This is why the server asks clients to always pass it. | | **Q** | the tool arguments as fields, never truncated, including defaults the server filled in (such as `format: markdown`). YAML and multi-line text stay fenced. For protocol traffic, the params (e.g. the `plungeai://` URI read). | | **Client context** | anything else your client put in `params._meta` besides the progress token | | **A** | the answer as a receipt: whole when 300 characters or fewer, otherwise its length plus the first 240 characters. The full output stays in your execution history (`plungeai_get_result`). | | **Error** | the full error text, when there was one. For invalid arguments: `invalid arguments: limit Too big: …`. | **What is recorded.** Every authenticated request: tool calls, argument rejections, rate-limit hits, fenced-tool denials and protocol methods. Failed logins are recorded without an account, so they do not appear in your log. **Secrets are redacted** before anything is written: the value of any argument whose **name** looks like a secret (`api_key`, `api_secret`, `token`, `secret`, `password`, `passwd`, `credential`, `authorization`, `cookie`) is stored as `•••redacted•••`, including everything nested under it (`core/core-base/content-guard.ts`). A secret pasted into free text, such as a prompt, is **not** detected. Don't paste keys into prompts. # Tool reference Source: https://www.plungeai.com/docs/mcp/tool-reference Source: `orchestration/mcp-gateway/server.ts` (`TOOL_DEFS`, `StatusOutputSchema`, `validateToolArgs`), `orchestration/mcp-gateway/tools.ts` (zod schemas and handlers), `orchestration/mcp-gateway/tool-exec.ts` (`GetToolContractSchema`, `ExecuteToolSchema`), `orchestration/mcp-gateway/extras.ts` (`TemplatesSchema`, `ScheduleSchema`, `WhoamiSchema`), `orchestration/mcp-gateway/tools-bots.ts` and `tools-triggers.ts` (the bot, execution-steer and trigger actions), `orchestration/mcp-gateway/user-context-arg.ts` (`user_context`), `orchestration/mcp-gateway/shared.ts` (`formatArg`, `userRequest`, length bounds), `orchestration/mcp-gateway/tool-outcome.ts` (`ToolOutcomeSchema`, `renderAs`), `orchestration/mcp-gateway/chat.ts`, `orchestration/mcp-gateway/conversation.ts`, `orchestration/mcp-gateway/index.ts` (argument pre-screen, fenced tools). Every tool the server exposes, in the order `tools/list` returns them. You rarely call these by name: your client reads the list when it connects and picks the tool from your request. This chapter is the exact contract behind that choice. The server is `https://mcp.plungeai.com/v1`; `https://api.plungeai.com/v1/mcp` is the same server and returns the same list. **How to read the tables.** - **Type** is the JSON type with the server's bound: `string (max 256)` is at most 256 characters, `integer 1–100` an integer in that range. An enum lists every accepted value. - **Required** is what the input schema enforces: "yes" or "no". "For `<action>`" or "one of" means the schema accepts the call without the key, but that action, or the call, refuses to run without it. - **Default** is what happens when you leave the key out: the schema default, or the default the handler applies. "—" means no default. - A call whose arguments do not match the schema is not run. It comes back as a 📝 NEEDS INPUT outcome naming each bad or missing field, with `isError: false` ([Errors, feedback & limits](/mcp/errors-limits)). **Two parameters recur.** - `user_request`, on every tool: a string (max 4,000), optional. It carries the user's request verbatim, in their own words, before the client turned it into arguments. The server instructions ask clients to always send it: the platform uses it for request context, routing and support diagnostics. Some tools also show it as the run's title. - `format`, on nine tools: `markdown` (the default) or `json`. `json` returns the same answer as a JSON document in the text and the same object as `structuredContent` ([Protocol & transport](/mcp/protocol)). **Annotations.** Six tools carry `readOnlyHint: true`: `plungeai_get_tool_contract`, `plungeai_list_agents`, `plungeai_list_workflows`, `plungeai_get_result`, `plungeai_get_workflow_status` and `plungeai_whoami`. Several other tools have read-only actions (for example `plungeai_executions` `list`), but the flag is per tool. Seven tools declare an `outputSchema`: `plungeai_execute_workflow`, `plungeai_execute_agent`, `plungeai_get_tool_contract`, `plungeai_execute_tool`, `plungeai_run_mission` and `plungeai_learn` declare the outcome envelope ([Tools](/mcp/tools)), and `plungeai_get_workflow_status` declares its status object. **Keys can be fenced.** A key limited to some tools does not see the others in `tools/list`, and a call to one answers JSON-RPC `-32602` `Tool not permitted for this key: <name>` ([Authentication & keys](/mcp/authentication)). | # | Tool | Read-only | Output schema | Chapter | |---|---|---|---|---| | 14.1 | `plungeai_execute_workflow` | no | outcome | [8](/mcp/workflows) | | 14.2 | `plungeai_execute_agent` | no | outcome | [7](/mcp/agents-missions) | | 14.3 | `plungeai_get_tool_contract` | yes | outcome | [5.4](/mcp/discovery#54-the-invocation-contract--plungeai_get_tool_contract) | | 14.4 | `plungeai_execute_tool` | no | outcome | [6](/mcp/tools) | | 14.5 | `plungeai_run_mission` | no | outcome | [7](/mcp/agents-missions) | | 14.6 | `plungeai_learn` | no | outcome | [10](/mcp/chat-memory-skills) | | 14.7 | `plungeai_list_agents` | yes | — | [5](/mcp/discovery) | | 14.8 | `plungeai_list_workflows` | yes | — | [8.1](/mcp/workflows#81-find-your-workflows--plungeai_list_workflows) | | 14.9 | `plungeai_get_result` | yes | — | [9](/mcp/runs-results) | | 14.10 | `plungeai_get_workflow_status` | yes | status | [9](/mcp/runs-results) | | 14.11 | `plungeai_workflow` | no | — | [8](/mcp/workflows) | | 14.12 | `plungeai_executions` | no | — | [9](/mcp/runs-results) | | 14.13 | `plungeai_followup` | no | — | [9](/mcp/runs-results) | | 14.14 | `plungeai_continue` | no | — | [9](/mcp/runs-results) | | 14.15 | `plungeai_chat` | no | — | [10](/mcp/chat-memory-skills) | | 14.16 | `plungeai_build_workflow` | no | — | [8](/mcp/workflows) | | 14.17 | `plungeai_memory` | no | — | [10](/mcp/chat-memory-skills) | | 14.18 | `plungeai_templates` | no | — | [5.5](/mcp/discovery#55-workflow-templates--plungeai_templates-list-and-get), [8.9](/mcp/workflows#89-start-from-a-template--plungeai_templates-use) | | 14.19 | `plungeai_schedule` | no | — | [11](/mcp/scheduling) | | 14.20 | `plungeai_whoami` | yes | — | [2](/mcp/authentication) | | 14.21 | `plungeai_companies` | no | — | [16](/mcp/companies) | | 14.22 | `plungeai_connections` | no | — | [17](/mcp/connections) | ## 14.1 plungeai_execute_workflow Execute a CNL workflow: a saved one (`workflow_id`) or ad-hoc YAML (`workflow_yaml`). Streams progress. Use `mode:"async"` for long runs (over 3 minutes): it returns an execution id to poll with `plungeai_get_workflow_status`. In ad-hoc YAML, write long strings as block scalars (`prompt: |`) and never hard-wrap a value; structured tool-agents take their card's parameters as task fields next to `agent:`. Always answers with a structured outcome, never a raw error: follow the remediation instead of retrying the identical call. - **Read-only:** no. **Output schema:** the outcome envelope. **Creates an execution:** yes. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words; also the run's title when the question lives inside the YAML | | `workflow_yaml` | string (max 262,144) | one of `workflow_yaml` / `workflow_id` | — | ad-hoc CNL. The `workflow:` wrapper is optional: bare top-level `name` / `tasks` is wrapped for you | | `workflow_id` | string (max 256) | one of `workflow_yaml` / `workflow_id` | — | the id of one of your saved workflows. Wins if both are passed | | `input` | string (max 65,536) | no | — | the run's input, used where the workflow references `{input}` | | `inputs` | object, string keys (max 128) → string values (max 8,192) | no | — | named inputs | | `mode` | enum `sync`, `async` | no | `sync` | `async` starts a background run and returns at once | | `user_context` | object: `time_zone`, `locale`, `place`, `latitude`, `longitude`, `accuracy_m`, `source` (`device` or `network`, default `device`) | no | the request's network location | where and when the user is: the agent's local time and its default schedule zone. Wins over the network guess; a value that does not validate (an unknown zone, a malformed locale) is dropped | | `format` | enum `markdown`, `json` | no | `markdown` | answer format | **Returns.** `ok` with the run's output and the execution-id footer. `ok` with only the execution id for `mode:"async"`. `needs_input` when neither YAML nor id is given, when the saved id is not on your account (``No saved workflow with id `<id>` on this account.``), or when the YAML does not parse. `unavailable` when ad-hoc YAML names an agent that does not exist or is not active, before anything runs. `needs_approval` / `needs_input` when the run pauses for you. A failed run is classified into an outcome; an invalid-YAML failure adds a tip on quoting and block scalars. Runs a model and costs credits (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_workflow","arguments":{"user_request":"run a one-task pong workflow","workflow_yaml":"name: pong-demo\ntasks:\n - type: task\n id: t1\n agent: one-agent\n prompt: \"Reply with only the word: pong\"\n"}}}' ``` ## 14.2 plungeai_execute_agent Execute a single prompt-driven agent, for example `one-agent` (call mode; add a `persona` for a specialist answer). Pass `session_id` to start or continue a conversational agent. Structured tool-agents (cards with a Parameters table, such as `markitdown` or `weather-agent`) take typed fields, not prose: use `plungeai_get_tool_contract` and `plungeai_execute_tool` for those. Always answers with a structured outcome, never a raw error: follow the remediation instead of retrying the identical call. - **Read-only:** no. **Output schema:** the outcome envelope. **Creates an execution:** yes (not for a refusal). | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `agent` | string (max 256) | yes | — | a registry agent id; must be `status:active` | | `prompt` | string (max 65,536) | yes | — | the prompt | | `persona` | string (max 256) | no | — | a persona id; `one-agent` answers in that voice | | `model` | string (max 256) | no | the agent's default | checked against the live model catalog before anything runs. Dated variants match their catalog id by prefix. The catalog entry also sets the provider | | `provider` | string (max 256) | no | the model's catalog provider | overrides the provider | | `maxTokens` | integer 1–64,000 | no | — | maximum output tokens | | `max_tokens` | integer 1–64,000 | no | — | alias of `maxTokens`; `maxTokens` wins if both are passed | | `temperature` | number 0–2 | no | — | sampling temperature | | `top_p` | number 0–1 | no | — | nucleus sampling | | `reasoning_effort` | enum `low`, `medium`, `high`, `xhigh` | no | — | reasoning effort for models that support it | | `thinking_level` | string (max 256) | no | — | passed through to the agent | | `streaming` | boolean | no | `true` | `false` opts this call out of forced streaming | | `session_id` | string (max 256) | no | — | starts or continues a conversation with the agent | | `user_context` | object: `time_zone`, `locale`, `place`, `latitude`, `longitude`, `accuracy_m`, `source` (`device` or `network`, default `device`) | no | the request's network location | where and when the user is: the agent's local time and its default schedule zone. Wins over the network guess; a value that does not validate (an unknown zone, a malformed locale) is dropped | | `format` | enum `markdown`, `json` | no | `markdown` | answer format | **Returns.** `ok` with the agent's output and the execution-id footer. `unavailable` for an unknown or inactive agent, with live alternatives and no run created. `needs_input` for a model that is not in the catalog (``Unknown model `<id>` … Nothing was executed.``) with up to five near matches. `needs_approval` / `needs_input` when the agent pauses. Failures are classified into an outcome. Runs a model and costs credits (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_agent","arguments":{"user_request":"ask one-agent what a Durable Object is","agent":"one-agent","prompt":"In one sentence: what is a Cloudflare Durable Object?","model":"claude-sonnet-4-5","max_tokens":80}}}' ``` ## 14.3 plungeai_get_tool_contract The exact invocation contract for one registry agent: the JSON Schema of its parameters, its operations (with approval gates), worked YAML examples, its output shape, and your live credential status (platform-managed, or "connect Google first"). Fetch it before the first `plungeai_execute_tool` call to an unfamiliar agent: the contract is the API docs. - **Read-only:** yes. **Output schema:** the outcome envelope. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `agent_id` | string (max 256) | yes | — | a registry agent id | | `format` | enum `markdown`, `json` | no | `markdown` | answer format | **Returns.** `ok`, whose `contract` holds `agent_id`, `name`, `description`, `operations[]` (`{name, description?, required_params?}`), `inputSchema` (JSON Schema for `params`), `examples[]` (`{title, cnl}`), `output_type`, `soleRequiredParam` (when exactly one string parameter is required) and `credentials[]` (per provider: `provider`, `tier` = `platform` / `oauth_connector` / `byok_api_key`, `required`, optional `operations`, `connected`, `status`, `accountEmail`). The Markdown adds a **Credentials** section (platform-managed, ✅ connected as …, ⚠️ NOT connected or expired, with where to connect) and a ready `plungeai_execute_tool` line. `unavailable` for an unknown or inactive agent, with alternatives. `error` if the registry did not answer. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_get_tool_contract","arguments":{"user_request":"what does the weather agent need from me?","agent_id":"weather-agent"}}}' ``` ## 14.4 plungeai_execute_tool Execute one structured tool-agent directly with typed params: `{agent_id, operation?, params}`. Runs through the engine with full observability; use `mode:"async"` for long runs. Always answers with a structured outcome, never a raw error: `ok` → the result; `needs_input` → the missing fields and the schema; `needs_connection` / `needs_api_key` → tell the user exactly what to connect in our apps, then retry; `needs_approval` → relay the ⏸ block and use `plungeai_continue` after the user decides; `unavailable` → real alternatives. Follow the remediation; do not retry the identical call blind. - **Read-only:** no. **Output schema:** the outcome envelope. **Creates an execution:** yes (not for a refusal). | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words; also shown as the run's input | | `agent_id` | string (max 256) | yes | — | a registry agent id; must be `status:active` | | `operation` | string (max 128) | no | the card's default operation | one of the contract's `operations[].name`. Also fills the card's `action` / `op` / `operationType` selector parameter when you did not set it | | `params` | object, string keys (max 128) → any value | no | `{}` | typed parameters, validated against the contract. Names that collide with task-envelope fields are refused | | `prompt` | string (max 65,536) | no | — | free text. Satisfies missing requireds on prompt-capable agents; mapped (with a warning) onto a card's only free-text required parameter when you send no `params` | | `mode` | enum `sync`, `async` | no | `sync` | `async` starts a background run and returns at once | | `user_context` | object: `time_zone`, `locale`, `place`, `latitude`, `longitude`, `accuracy_m`, `source` (`device` or `network`, default `device`) | no | the request's network location | where and when the user is: the agent's local time and its default schedule zone. Wins over the network guess; a value that does not validate (an unknown zone, a malformed locale) is dropped | | `format` | enum `markdown`, `json` | no | `markdown` | answer format | **Returns.** Checks run in this order, and every refusal happens before a run exists: the active-only card check (`unavailable`), contract validation (`needs_input` with `missing` and the contract), the credential pre-flight (`needs_connection` / `needs_api_key`). Then the engine runs the task: `ok` with the result, any `warnings`, and the execution-id footer; `needs_approval` when an outward, irreversible operation (send, pay, transfer, buy, withdraw) is stopped by the trust fence, with nothing run, no remediation actions and no paused session: despite the description's `plungeai_continue` hint, `plungeai_continue` cannot resume it, and it runs only from Ocean Studio ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)); `needs_approval` / `needs_input` when the agent pauses (`⏸ AWAITING USER APPROVAL` / `⏸ AWAITING USER`), which `plungeai_continue` does resume; a classified outcome on failure. Runs a tool agent and creates an execution (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_execute_tool","arguments":{"user_request":"current weather in Sun Valley, Idaho","agent_id":"weather-agent","operation":"current","params":{"location":"Sun Valley, Idaho"}}}}' ``` ## 14.5 plungeai_run_mission Run a bounded autonomous agent mission (a loop agent): a goal, an optional tool fence and an iteration cap. Recalls and writes long-term memory automatically. Defaults to async: poll `plungeai_get_workflow_status`. Always answers with a structured outcome, never a raw error: follow the remediation instead of retrying the identical call. - **Read-only:** no. **Output schema:** the outcome envelope. **Creates an execution:** yes. There is no `format` parameter. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `goal` | string (max 65,536) | yes | — | what the mission must achieve | | `agent` | string (max 256) | no | `one-agent` | the loop runtime that runs the mission; omitted, the gateway sends `agent: one-agent` explicitly. A named agent passes the active-only fence | | `mission` | string (max 65,536) | no | `You are a capable autonomous agent. Achieve the goal thoroughly and cite sources.` | the mission's standing instructions | | `allowed_tools` | array of string (max 256 each), at most 50 | no | `["web_search", "web_fetch", "task_complete"]` | the tool fence | | `max_iterations` | integer 1–50 | no | `8` | the iteration cap | | `success_criteria` | array of string (max 2,000 each), at most 20 | no | — | what counts as done | | `persona` | string (max 256) | no | — | a persona id | | `skills` | array of string (max 256 each), at most 20 | no | — | skill ids to inject, including your learned skills | | `mode` | enum `sync`, `async` | no | `async` | `sync` waits for the result | | `user_context` | object: `time_zone`, `locale`, `place`, `latitude`, `longitude`, `accuracy_m`, `source` (`device` or `network`, default `device`) | no | the request's network location | where and when the user is: the agent's local time and its default schedule zone. Wins over the network guess; a value that does not validate (an unknown zone, a malformed locale) is dropped | **Returns.** Async: `ok` with ``Started mission `<id>` (async). …`` and the execution id. Sync: `ok` with the mission's output (also when it stopped at `max_iterations`: a sync mission never reports `incomplete`, [7.7](/mcp/agents-missions#77-mission-bounds)), `needs_approval` / `needs_input` when it pauses, or a classified outcome on failure. Runs an agent loop and costs credits (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_run_mission","arguments":{"user_request":"what is notable about Sun Valley, Idaho?","goal":"Three sourced bullets on what is notable about Sun Valley, Idaho.","max_iterations":6,"success_criteria":["three bullets","a source URL per bullet"]}}}' ``` ## 14.6 plungeai_learn Manage your private skill library. `action: learn` (the default) distills a URL, PDF text or a past workflow into a reusable skill with the `learn` pre-built agent (needs `source`; defaults to async, poll `plungeai_get_workflow_status`). `action: list` returns your learned skills (id and summary). `action: forget` deletes one by `name` (the id from `list`). Always answers with a structured outcome, never a raw error: follow the remediation instead of retrying the identical call. - **Read-only:** no. **Output schema:** the outcome envelope. **Creates an execution:** `learn` only. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `action` | enum `learn`, `list`, `forget` | no | `learn` | `learn`: distill `source` into a skill · `list`: your learned skills · `forget`: delete one | | `source` | string (1–65,536) | for `learn` | — | a URL, text or Markdown to distill, or a description of what you just did. Checked before the tool runs | | `name` | string (max 256) | for `forget` | picked automatically on `learn` | the skill id, kebab-case | | `mode` | enum `sync`, `async` | no | `async` | for `learn` | **Returns.** `learn`: `ok` with the execution id to poll (async), or the run's result (sync). `list`: `ok` with `N learned skill(s).` and one line per skill, or `No learned skills yet.`. `forget`: `ok` ``Forgot skill `<name>`.``, or `needs_input` (no `name`), `error` (no such skill), `unavailable` (not yours). See [Skills](/mcp/chat-memory-skills#103-skills-plungeai_learn). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_learn","arguments":{"user_request":"what skills have I learned?","action":"list"}}}' ``` ## 14.7 plungeai_list_agents Discover platform building-block agents in the live registry, the capability catalog. These are registry agents, not the user's own agents: for "show me my agents / workflows" use `plungeai_list_workflows`. `search` is a hybrid semantic and keyword query: describe the capability in natural language (for example "web search", "pdf to markdown"). Prefer `search` / `category` filters; an unfiltered list returns the whole catalog (about 90 agents, large). The catalog is active-only and execution refuses any id that is not active, so take ids from here, not from memory. Fetch one full card with `agent_id`. With `format:"json"` the result is `{content: <markdown>}`: its source is rendered registry markdown, and typed rows are a follow-up. - **Read-only:** yes. **Output schema:** none. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `search` | string (max 2,000) | no | — | a natural-language capability description | | `category` | string (max 256) | no | — | a registry category (for example `financials`, not `finance`) | | `agent_id` | string (max 256) | no | — | returns that one agent's full card | | `kind` | enum `agents`, `personas`, `experts`, `skills`, `models`, `workflows`, `connectors` | no | `agents` | which card set to search. `workflows` means registry workflow cards, not your saved workflows | | `limit` | integer 1–100 | no | 25 with `search` or `category`, 100 without | how many cards | | `format` | enum `markdown`, `json` | no | `markdown` | answer format | **Returns.** Registry Markdown: the matching cards, or one full card for `agent_id`. The active-only filter applies to `kind: "agents"`. An `agent_id` that does not exist, or is not active, gets an explanation and the closest live matches instead of the card. `Registry unavailable — try again shortly.` (`isError: true`) when the registry does not answer. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_list_agents","arguments":{"user_request":"which agents can search the web?","search":"web search"}}}' ``` ## 14.8 plungeai_list_workflows List your saved workflows, which is what users usually mean by "my agents". With no filters it shows your folders (rubrics) and your most recent workflows; filter with `folder`, `search` and/or `kind`. - **Read-only:** yes. **Output schema:** none. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `search` | string (max 2,000) | no | — | matches workflow names, case-insensitive substring | | `folder` | string (max 2,000) | no | — | matches folder names, case-insensitive substring | | `kind` | enum `workflow`, `agent`, `bot`, `campaign` | no | — | only that kind | | `limit` | integer 1–100 | no | `50` | how many workflows | | `format` | enum `markdown`, `json` | no | `markdown` | answer format | **Returns.** Without filters: `# Your Workflows (N most recent)`, a **Folders:** line with every non-empty folder and its count, and a table: Name, Kind, Folder, ID, Description. With filters: `# Your Workflows (N)` and the same table. `No workflows found. …` / `No workflows matched. …` when empty. JSON: `{"workflows": [{id, name, description, kind, folder}]}`. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_list_workflows","arguments":{"user_request":"show me my workflows in the Finance folder","folder":"Finance","format":"json"}}}' ``` ## 14.9 plungeai_get_result Retrieve the result of one of your executions by `execution_id`. For conversational runs it includes the full conversation thread (all follow-up turns), as Studio shows it. Pass `task_id` to read one task's output (for example one branch of a workflow) instead of the final result. The output is final, user-ready Markdown: show it verbatim and complete, never summarize or re-type it. With `format:"json"` the result is the `GET /v1/executions/{id}/output` body (`execution_id`, `status`, `content`, `final_task_id`, `steps`, `continuation`), not the rendered thread; with `task_id` it is `{execution_id, task_id, result}`. - **Read-only:** yes. **Output schema:** none. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `execution_id` | string (max 256) | one of `execution_id` / `workflow_id` | — | the execution id (preferred). The engine's `exec-…` id is also accepted | | `workflow_id` | string (max 256) | one of `execution_id` / `workflow_id` | — | older alias of `execution_id`; it still takes an execution id, not a workflow id | | `task_id` | string (max 256) | no | the final task | one task's output instead of the full result | | `format` | enum `markdown`, `json` | no | `markdown` | answer format | **Returns.** The full result: an identity line, the final output, a **Steps in this run** index for multi-step runs, the **Conversation thread** when there are follow-up turns, and a ⏸ block when the run is paused. When there is no output it says why: failed, still running, or no longer stored. An unknown or foreign id is `needs_input`. Details: [9.2](/mcp/runs-results#92-get-a-result-plungeai_get_result). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_get_result","arguments":{"user_request":"show me the result of that last run","execution_id":"<execution_id>"}}}' ``` ## 14.10 plungeai_get_workflow_status Check the status of one of your executions (it self-heals stuck runs). It also returns structured content, including `continuation` when the run is paused awaiting the user's approval or answer: relay that to the user and use `plungeai_continue`. - **Read-only:** yes. **Output schema:** the status object. **Creates an execution:** no. There is no `format` parameter; `structuredContent` is always present. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `execution_id` | string (max 256) | yes | — | the execution id, or the engine's `exec-…` id | **Returns.** A Markdown status block and `structuredContent` with `execution_id`, `status` (`running`, `completed`, `incomplete`, `failed`, `cancelled`; `incomplete` only for async runs, since a sync call stamps its run `ok`, which then reads `completed`), `error_message`, `workflow_name`, `started_at`, `duration_ms`, `final_task_id` and `continuation` (`{status, agent, question, pending_action}` or null). A `running` row with a stored result is healed to `completed`; one with no heartbeat for more than 960 seconds is marked `failed` (`Interrupted (run did not complete)`; the reason shows from the next read on). Details: [9.3](/mcp/runs-results#93-check-status-plungeai_get_workflow_status). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_get_workflow_status","arguments":{"user_request":"is that still running?","execution_id":"<execution_id>"}}}' ``` ## 14.11 plungeai_workflow Manage your saved workflows: `action` `create`, `get`, `update`, `delete`, `save_version`, `list_versions`, `get_version`, `restore_version` or `a2a_door`. Create, update and delete sync live to Studio, iMini and the other apps. `a2a_door` manages the workflow's Agent2Agent door (see [8.6](/mcp/workflows#86-manage-saved-workflows--plungeai_workflow)). The bot actions work on a saved bot you own: `permissions_get` / `permissions_set` (class presets, admin locks, spend caps), `files` / `file` (its persistent workspace), `inbox`, `bindings` / `bind` / `unbind` (paired chats) and, by group name, `groups` / `group_set` / `group_delete` / `group_thread`. - **Read-only:** no (`get`, `list_versions` and `get_version` only read). **Output schema:** none. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `action` | enum `create`, `get`, `update`, `delete`, `list_versions`, `get_version`, `save_version`, `restore_version`, `a2a_door`, `permissions_get`, `permissions_set`, `files`, `file`, `inbox`, `bindings`, `bind`, `unbind`, `groups`, `group_set`, `group_delete`, `group_thread` | yes | — | what to do | | `workflow_id` | string (max 256) | for every action except `create` and the group actions | — | the workflow (for the bot actions, the saved bot) | | `name` | string (max 512) | for `create` | — | the workflow's name; `update` renames | | `yaml` | string (max 262,144) | for `create` | — | the CNL YAML. Validated before it is saved, on `create` and `update` | | `description` | string (max 2,000) | no | — | `create` / `update` | | `folder` | string (max 2,000) | no | — | `create`: file into this folder. `update`: move into it; `none` or `""` takes it out of its folder. Matched case-insensitively: the exact name first, else a single partial match | | `kind` | enum `workflow`, `agent`, `bot`, `campaign` | no | `workflow` on create | the sidebar section: Flows, Agents, Bot agents or Campaigns | | `campaign_config` | object, string keys (max 64) → any value | no | — | `create` with `kind: campaign`: the ledger and schedule, `{list: {inline, table, agent or csv}, cycle, max_attempts, schedule?, deliver?, …}`. Refused before anything is saved when it is malformed; without it the campaign is saved with a warning and no ledger | | `version_id` | string (max 256) | for `get_version`, `restore_version` | — | a version id from `list_versions` | | `version_description` | string (max 2,000) | no | — | a note for `save_version` | | `door_action` | enum `mint`, `get`, `pause`, `resume`, `revoke` | for `a2a_door` | — | `mint` creates the door (or rotates its token), `get` shows it, `pause`/`resume` close and reopen it, `revoke` deletes it | | `handle` | string (max 64) | no | the workflow's name as a slug | `a2a_door` `mint`: the door's URL handle, 3–63 lowercase letters, digits or dashes | | `classes` | object, string keys (max 16) → string (max 16) | no | — | `permissions_set`: class → preset, `send` / `write` / `pay` / `delete` → `allow` / `ask` / `deny` / `hand_off`. A field present replaces the stored one | | `locks` | array of string (max 16 each), at most 8 | no | — | `permissions_set`: the classes an admin locks. Only a team admin over the workflow may set them (`lock_admin_required`); a refused call writes nothing | | `budget_usd_run` | number ≥ 0 or null | no | — | `permissions_set`: the spend cap per run in USD; `null` clears it | | `budget_usd_month` | number ≥ 0 or null | no | — | `permissions_set`: the spend cap per month in USD; `null` clears it | | `path` | string (max 512) | for `file` | — | a workspace path as `files` lists it. The file comes back as base64, at most 5 MB | | `channel` | string (max 20) | for `bind`, `unbind` | — | the paired channel: `telegram`, `whatsapp`, `discord` or `teams` | | `sender_id` | string (max 128) | for `bind`, `unbind` | — | the paired chat's sender id | | `group` | string (max 40) | for `group_set`, `group_delete`, `group_thread` | — | the bot group's name | | `members` | array of string (max 256 each), at most 6 | for `group_set` | — | the group's 2 to 6 member bot ids; `group_set` creates the group or replaces its members | **Returns.** `create`: ``Created workflow **<name>** (`<id>`). … Synced to all apps.``, with ``Filed under **<folder>**.`` or a ⚠️ note when the folder did not match (then it is created without a folder). `get`: the name, description and YAML, or `needs_input` when not found. `update`: ``Updated workflow `<id>`. Synced to all apps.`` with folder and kind notes; an unknown folder refuses the whole update. `delete`: ``Deleted workflow `<id>`. …``; a bot's memory namespace `<you>:bot:<id>` and scheduler jobs, or a campaign's jobs and ledger, are cleaned up after the row is gone (best effort; `update` with a `kind` that leaves `bot` or `campaign` does the same for the old kind). `save_version`: ``Saved version V<n> (`<version id>`).``. `list_versions`: a table of Version, ID, When, Note. `get_version`: that version's YAML. `restore_version`: `Restored — new version V<n>.`. `a2a_door` `mint`: ``A2A door **<url>** minted for **<name>**.`` with the token (shown once; minting again rotates it), the agent-card URL and a `curl` SendMessage recipe; `get`: the door's URL, status and kind, or `No A2A door for **<name>** yet.`; `pause`/`resume`: ``A2A door of **<name>** is now paused|active.``; `revoke`: ``A2A door of **<name>** revoked — …``. `permissions_get` / `permissions_set`: the classes, locks and caps as a list and a JSON block; `permissions_get` also lists `effective`, the preset each of the four classes runs under (the stored one, else `allow`). `files`: a table of path, size, type and update time; `file`: the file name, type and size and the bytes as a base64 block (a file over 5 MB is refused `file_too_large` with its One API download path). `inbox` and `group_thread`: a table of the queued messages. `bindings`: the chats bound to the bot, or a note when channel bindings are not enabled yet; `bind` / `unbind`: a confirmation. `groups`, `group_set`, `group_delete`: the groups and the change. Every bot refusal is ``Error: <message> (<code>)`` with the same codes as the One API (`workflow_not_found`, `lock_admin_required`, `invalid_params`, `file_not_found`, `not_paired`, `name_taken`, …). A workflow you do not own answers `Workflow not found.` on `update` / `delete` and the version actions (`get` returns a `needs_input` outcome), but ``Error: no saved workflow with that id for this account (workflow_not_found)`` on the bot actions above (and a group you do not own, addressed by name, answers `Group "<name>" not found.`). CNL validation and kind-lint warnings are appended; a validation or kind-lint error (for example `memory_owner` in the YAML) refuses the save (`Workflow YAML failed CNL validation: …`). A missing required field answers `Error: <field> is required for action "<action>"`. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_workflow","arguments":{"user_request":"show me my Acquisition Scanner workflow","action":"get","workflow_id":"<workflow_id>"}}}' ``` ## 14.12 plungeai_executions Browse your execution history: `action` `list`, `get`, `output`, `conversation`, `files`, `delete`, `cancel` or `steer`. `cancel` stops a running execution cooperatively: it stops at its next turn or tool boundary. `steer` queues a `message` for a running execution, delivered at its next turn; `files` lists the files the run wrote. Outputs are final, user-ready Markdown (tables, threads): show them verbatim and complete, never summarize or re-type them. - **Read-only:** no (`delete` and `cancel` change state). **Output schema:** none. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `action` | enum `list`, `get`, `output`, `conversation`, `delete`, `cancel`, `steer`, `files` | yes | — | what to do | | `execution_id` | string (max 256) | for every action except `list` | — | the execution | | `workflow_id` | string (max 256) | no | — | `list`: only runs of this workflow | | `message` | string (max 65,536) | for `steer` | — | `steer`: the message queued for the running execution | | `limit` | integer 1–100 | no | `20` | `list`: page size | | `offset` | integer 0–100,000 | no | `0` | `list`: rows to skip | | `format` | enum `markdown`, `json` | no | `markdown` | answer format | **Returns.** `list`: `# Executions (N)`, newest first (ID, Question, Workflow, Status, Source, When); JSON `{"executions": [{id, input, workflow_name, status, start_time, duration, source}]}`. `get`: the status answer of `plungeai_get_workflow_status`. `output`: the full result of `plungeai_get_result`. `conversation`: the follow-up thread; JSON `{execution_id, messages}`. `cancel`: ``Cancelling execution `<id>` — …`` (running runs only). `steer`: ``Queued your message to running execution `<id>` …``, or an error when the run already finished (use `plungeai_followup`). `files`: a table of the files the run wrote. `delete`: ``Deleted execution `<id>`.``; this cannot be undone. Details: [9.4](/mcp/runs-results#94-run-history-plungeai_executions). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_executions","arguments":{"user_request":"show me my last 5 executions","action":"list","limit":5,"format":"json"}}}' ``` ## 14.13 plungeai_followup Ask a follow-up about a completed execution: it continues the run's agent session, or researches with the prior context. If the execution is still running, the message is queued and injected at its next turn (steering); if it has finished, the conversation continues. - **Read-only:** no. **Output schema:** none. **Creates an execution:** no; the turn is added to the run's conversation thread. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `execution_id` | string (max 256) | yes | — | the run to follow up on | | `prompt` | string (max 65,536) | yes | — | the follow-up message | **Returns.** Running: ``Queued your message to running execution `<id>` (delivered at its next turn).`` Finished: the answer as Markdown, plus a ⏸ block if the agent pauses again. Errors (`isError: true`): `Execution not found.`, `Followup agent call failed`, `Failed to retrieve follow-up response`. Details: [9.5](/mcp/runs-results#95-follow-ups-plungeai_followup). Runs a model and costs credits (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_followup","arguments":{"user_request":"now focus on the competitive landscape","execution_id":"<execution_id>","prompt":"Do the same analysis, but focus on the competitive landscape."}}}' ``` ## 14.14 plungeai_continue Continue a paused conversation: answer a question (`message`) or approve a pending action (`approve: true`). Pass `approve: true` only after the user explicitly confirmed; never approve on your own. To deny or change course, pass the user's words as `message`. - **Read-only:** no. **Output schema:** none. **Creates an execution:** no; the agent runs again inside the paused run. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `execution_id` | string (max 256) | yes | — | the paused run | | `message` | string (max 65,536) | one of `message` / `approve: true` | — | the user's answer, denial or change of course | | `approve` | boolean | one of `message` / `approve: true` | — | `true` approves the staged action. Wins over `message` if both are passed; `false` alone is refused | **Returns.** The agent's reply, then either a new ⏸ block or `✅ Conversation resolved (no further input needed).` It resumes an agent pause (`⏸ AWAITING USER APPROVAL` / `⏸ AWAITING USER`); a trust-fence `⏸ NEEDS APPROVAL` with no actions has no session and answers `No conversation session…` ([6.6](/mcp/tools#66-the-trust-fence--needs-approval)). Errors (`isError: true`): `` Provide `message` or `approve: true` ``, `Execution not found`, `No conversation session for this execution (expired or not a conversational agent)`, `This run isn't paused — it already completed. Use plungeai_followup …`, `Agent continuation failed`. Details: [9.6](/mcp/runs-results#96-paused-runs-plungeai_continue). Runs the held action, which may spend money; send it only after the user said yes (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_continue","arguments":{"user_request":"yes, go ahead","execution_id":"<execution_id>","approve":true}}}' ``` ## 14.15 plungeai_chat Persistent chat with the PlungeAI assistant (platform tools and web search): `action` `send`, `new`, `list_sessions` or `history`. Conversations appear in Studio and the CLI. - **Read-only:** no (`list_sessions` and `history` only read). **Output schema:** none. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `action` | enum `send`, `list_sessions`, `history`, `new` | yes | — | what to do | | `message` | string (max 65,536) | for `send` | — | `send`: the turn. `new`: only the title (first 50 characters) | | `conversation_id` | string (max 256) | for `history` | a new conversation (`send`) | the conversation to continue or read | | `user_context` | object: `time_zone`, `locale`, `place`, `latitude`, `longitude`, `accuracy_m`, `source` (`device` or `network`, default `device`) | no | the request's network location | where and when the user is: the agent's local time and its default schedule zone. Wins over the network guess; a value that does not validate (an unknown zone, a malformed locale) is dropped | **Returns.** `send`: the assistant's reply and ``_(conversation: `<id>`)_``. `new`: ``Created conversation `<id>`. …``. `list_sessions`: `# Your conversations (N)` (Title, ID, Messages) for the 20 most recent, or `No conversations yet.`. `history`: `# <title>` and every turn. Details: [10.1](/mcp/chat-memory-skills#101-chat-plungeai_chat). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_chat","arguments":{"user_request":"list my conversations","action":"list_sessions"}}}' ``` ## 14.16 plungeai_build_workflow Generate a new workflow from a goal, or refine an existing one (`workflow_id` + `instruction`), with the platform builder. The result is saved, so it appears everywhere (live sync). - **Read-only:** no. **Output schema:** none. **Creates an execution:** no; it creates or updates a saved workflow. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `goal` | string (max 65,536) | for a new workflow | — | what the workflow should do, in plain language | | `workflow_id` | string (max 256) | for a refine | — | the workflow to change | | `instruction` | string (max 65,536) | for a refine | — | the change to apply. With `workflow_id`, a refine wins over `goal` | **Returns.** New: ``Created workflow **<name>** (`<id>`). Synced to all apps. Run it with plungeai_execute_workflow.`` and the YAML. The name is the builder's own generated name, or the first 60 characters of the goal when the YAML has no plain `name:` line. Refine: ``Updated workflow **<name>** (`<id>`). Synced to all apps.`` and the YAML; the YAML is replaced in place, so save a version first if you may want to roll back. A generated workflow that fails CNL validation is not saved; the answer shows the errors and the YAML. Other errors (`isError: true`): ``Provide either `goal` (new) or `workflow_id` + `instruction` (refine).``, `Workflow not found: <id>`, `Workflow builder did not return a workflow section.` It reports generating, validating and saving on the progress channel while it works (30–60 seconds). Runs a model, costs credits and saves a workflow (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_build_workflow","arguments":{"user_request":"build me a workflow that scans a company for acquisition news and writes a briefing","goal":"Scan a company for acquisition news and write a one-page briefing with sources."}}}' ``` ## 14.17 plungeai_memory Long-term memory scoped to you: `action` `recall`, `remember`, `search_runs`, `get_run` or `forget`. `remember` writes the durable store that `recall` reads (`target` `user` or `memory`, `operation` `add`, `replace`, `remove` or `read`); use it when the user asks to remember something. Past-run history is separate: `search_runs` / `get_run` read the run journal, which `recall` deliberately does not include. - **Read-only:** no (`remember` writes, except `operation: "read"`; `forget` deletes). **Output schema:** none. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `action` | enum `recall`, `remember`, `search_runs`, `get_run`, `forget` | yes | — | what to do | | `query` | string (max 2,000) | for `search_runs` | — | keywords matched against run goals | | `run_id` | string (max 256) | for `get_run` | — | a run id from `search_runs` | | `entry_id` | string (max 256) | for `forget`, one of `entry_id` / `content` | — | `forget`: one entry's id (`mem_…`, as `GET /v1/memory` lists it) | | `target` | enum `user`, `memory` | no | `memory` | `remember`: which store | | `operation` | enum `add`, `replace`, `remove`, `read` | no | `add` | `remember`: what to do | | `content` | string (max 65,536) | for `add`, `replace`; for `forget`, one of `entry_id` / `content` | — | the entry to store; `forget`: every entry containing it is removed from both stores | | `old_text` | string (max 2,000) | for `replace`, `remove` | — | a substring that identifies exactly one existing entry | | `limit` | integer 1–100 | no | `10` | `search_runs`: how many runs | **Returns.** `recall`: both stores (memory, then the user profile), or `No memory recorded yet.`. `remember`: ``Saved to memory (<usage>). recall will see it immediately.``, or for `read` the entries under `# Memory (<target>)`; a write over the budget is refused with the current entries. `search_runs`: `# Matching runs` with `` `<run id>` — <goal> `` lines. `get_run`: the stored run, or `Run not found.`. `forget`: ``Forgot <n> memory entries.``, or `Memory entry not found.` for an id your stores do not hold. Details: [10.2](/mcp/chat-memory-skills#102-memory-plungeai_memory). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_memory","arguments":{"user_request":"what do you remember about our fiscal year?","action":"recall"}}}' ``` ## 14.18 plungeai_templates Workflow templates: `action` `list`, `get` or `use`. `use` creates a workflow in your account from the template (optional `name` and `folder`). - **Read-only:** no (`list` and `get` only read; `use` creates a workflow). **Output schema:** none. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `action` | enum `list`, `get`, `use` | yes | — | what to do | | `template_id` | string (max 256) | for `get`, `use` | — | a template id from `list` | | `category` | string (max 256) | no | — | `list`: exact category | | `name` | string (max 512) | no | `<template name> (copy)`; a bot template keeps its own name | `use`: the new workflow's name | | `folder` | string (max 2,000) | no | — | `use`: file the new workflow into this folder | | `limit` | integer 1–100 | no | `50` | `list`: how many templates | **Returns.** `list`: `# Workflow Templates (N)` with Name, Kind, Category, Difficulty, Used, ID and Description, featured templates first, or `No active workflow templates.`. `get`: the template's name, description, category, difficulty, use count and YAML. `use`: ``Created workflow **<name>** (`<id>`) from template **<template>**. … Run it with plungeai_execute_workflow.``; the template's use count goes up by one. Errors (`isError: true`): `template_id is required for this action.`, `Template not found: <id>`, `Template content missing from storage.`. ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_templates","arguments":{"user_request":"what workflow templates are available?","action":"list","limit":10}}}' ``` ## 14.19 plungeai_schedule Scheduled (cron) jobs: `action` `stats`, `list`, `get`, `create`, `update`, `pause`, `resume`, `delete`, `run_now`, `runs` or `wake`. `create` needs `name`, `job_type` (`agent`, `query`, `workflow` or `heartbeat`), `target` and `schedule`; `create` also takes `mission_ref` to schedule a pre-built agent card, `ends_at` to stop the job at a time, and `schedule: "@once"` with `run_at` for a one-time run. `wake` fires the job's saved workflow once with a `note`. The triggers of a job are the actions `webhook_mint` / `webhook_list` / `webhook_delete` / `webhook_rotate`, `email_mint` / `email_list` / `email_delete` / `email_rotate` and `hooks_mint` / `hooks_list` / `hooks_delete`. A rotate mints a successor with the same settings, then revokes the old trigger. - **Read-only:** no (`stats`, `list`, `get` and `runs` only read). **Output schema:** none. **Creates an execution:** `run_now` and every scheduled tick do, except for heartbeat jobs ([11.3](/mcp/scheduling#113-create-a-job)). | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `action` | enum `stats`, `list`, `get`, `create`, `update`, `pause`, `resume`, `delete`, `run_now`, `runs`, `wake`, `webhook_mint`, `webhook_list`, `webhook_delete`, `webhook_rotate`, `email_mint`, `email_list`, `email_delete`, `email_rotate`, `hooks_mint`, `hooks_list`, `hooks_delete` | yes | — | what to do | | `job_id` | string (max 256) | for `get`, `update`, `pause`, `resume`, `delete`, `run_now`, `wake` and every trigger action | — | a job id (`job_…`); `runs` takes it as a filter | | `name` | string (max 512) | for `create` | — | the job's name; for agent, query and `mission_ref` jobs it also names the wrapper workflow `Scheduled: <name>` | | `description` | string (max 2,000) | no | — | free text | | `job_type` | enum `agent`, `query`, `workflow`, `heartbeat` | for a plain `create` | — | not needed with `mission_ref` | | `target` | string (max 262,144) | for a plain `create` | — | `agent`: an agent id. `query`: JSON `{"agent", "query"}`. `workflow`: one of your workflow ids | | `schedule` | string (max 128) | for `create` | — | a five-field cron expression | | `timezone` | string (max 64) | no | UTC | the IANA zone the cron is read in | | `run_at` | string (max 64) | for `create` with `schedule: "@once"` | — | the one-time run: an ISO-8601 time with a UTC offset, e.g. `2026-10-06T09:00:00Z`. Missing, `create` answers `run_at required` and saves nothing | | `ends_at` | string (max 64) | no | — | `create` / `update`: the job stops after this ISO-8601 time | | `parameters` | object, string keys (max 128) → any value | no | — | job extras: `prompt` (the per-run goal), `inputs`, and `deliver`: an array of `{channel, chat_id?, to?}` with `channel` one of `inapp`, `email`, `slack`, `telegram`, `whatsapp`, `discord`, `teams`. A bare `{channel:"slack"}` DMs the owner, a bare `{channel:"email"}` mails the owner's registered address, other email addresses must be confirmed recipients | | `limit` | integer 1–100 | no | `50` for `list`, `20` for `runs` | how many rows | | `mission_ref` | string (max 256) | no | — | a pre-built agent card id; schedules the card as a one-task harness workflow | | `check_agent` | string (max 256) | for a heartbeat, unless `check_workflow` | — | the active agent that checks the condition | | `check_workflow` | string (max 256) | no | — | a saved workflow id run as the check instead of `check_agent`; its last task answers YES or NO | | `condition_prompt` | string (max 4,096) | for a heartbeat | — | the condition, answered YES or NO each tick | | `trigger_workflow` | string (max 256) | no | — | heartbeat: the workflow id to run when the condition is met | | `notify_channel` | enum `telegram`, `whatsapp`, `discord`, `slack`, `teams`, `email` | no | — | heartbeat: where to notify (WhatsApp sends nothing today) | | `notify_chat_id` | string (max 128) | no | — | heartbeat: the chat or address to notify; needed together with `notify_channel` | | `note` | string (max 512) | no | — | `wake`: the note the woken run receives as its input | | `trigger_id` | string (max 256) | for `webhook_delete`, `webhook_rotate`, `email_delete`, `email_rotate`, `hooks_delete` | — | the token (webhook, email) or id (hooks) the list shows | | `prompt_template` | string (max 16,384) | no | — | `webhook_mint`: the run's prompt, filled from the payload | | `scheme` | string (max 64) | no | `generic` | `webhook_mint`: the provider signature scheme | | `filters` | object, string keys (max 64) → any value | no | — | `webhook_mint`: payload filters `{all?: Rule[], any?: Rule[]}`; a payload that does not match does not fire | | `allowed_from` | string (max 16,384) | no | anyone | `email_mint`: comma-separated senders allowed to trigger the run | | `url` | string (max 512) | for `hooks_mint` | — | `hooks_mint`: the https URL called, signed, on the listed run events | | `events` | array of string (max 64 each), at most 16 | for `hooks_mint` | — | `hooks_mint`: the run events that call the URL | **Returns.** Markdown text per action: tables for `list` and `runs`, the job as JSON for `get`, a one-line confirmation for the writes, and for `run_now` the execution id to poll. `wake`: the time the woken run starts. The trigger actions answer with the scheduler's JSON in a block: a mint or a rotate shows its secret once and never again. Errors are `isError: true` text that names the cause. Details: [Scheduling](/mcp/scheduling). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"what do I have scheduled?","action":"stats"}}}' ``` ## 14.20 plungeai_whoami Show the authenticated identity: user id, tier, key label and rate-limit window. - **Read-only:** yes. **Output schema:** none. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `format` | enum `markdown`, `json` | no | `markdown` | answer format | **Returns.** `# Who am I` with User, Auth (type and tier), Key (label and key id), Rate window (`<n>/min used · <n>/day used`, left out when the counter cannot be read) and Server. JSON: `{"user", "auth_type", "tier", "key_label", "key_id", "rate_window": {"minute_used", "day_used"} or null, "server"}`. Details: [Authentication & keys](/mcp/authentication). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I?"}}}' ``` ## 14.21 plungeai_companies Your companies, action for action the One API's `/v1/companies`: a campus compiled from your saved campus, building and office documents (`kind: campus` / `building` workflows), its items, the gates at its door, its mode and its clock. Every rule lives in company-service; the tool calls it as you. A campus you do not own is the same `not_found` as one that does not exist. - **Read-only:** no (`list`, `layout`, `snapshot`, `events`, `slice` and `approvals` only read). **Output schema:** none; every answer also carries `structuredContent`. **Creates an execution:** `decide` with `approve` on a tool permission continues the paused execution once; the company's clock starts office runs after `start`. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `action` | enum `list`, `apply`, `layout`, `snapshot`, `events`, `ingest`, `slice`, `item_create`, `item_pause`, `item_resume`, `item_retry`, `item_cancel`, `item_steer`, `approvals`, `decide`, `mode`, `start`, `stop`, `delete` | yes | — | what to do | | `campus` | string (max 64) | for every action but `list` | — | the campus slug | | `building` | string (max 64) | for `slice`, `item_create` | — | a building slug of the campus | | `since` | integer ≥ 0 | no | `0` | `events`: the seq to read after | | `limit` | integer 1–1,000 | no | `200` | `events`: how many | | `events` | array (at most 200) of `{building, type, item_id?, office?, data?, at?}` | for `ingest` | — | `ingest`: a local building's events | | `item_id` | string (max 256) | for `item_pause`, `item_resume`, `item_retry`, `item_cancel`, `item_steer` | — | the item | | `title` | string (max 300) | for `item_create` | — | the item's title | | `body` | string (max 65,536) | no | — | `item_create`: the item's text | | `kind` | enum `feature`, `bugfix`, `docs`, `research`, `content`, `task` | no | — | `item_create`: the item's kind | | `priority` | integer | no | — | `item_create`: higher goes first | | `tier` | string (max 32) | no | — | `item_create`: the item's tier word | | `text` | string (max 65,536) | for `item_steer` | — | the message for the item's running office | | `status` | enum `pending`, `approved`, `rejected`, `expired` | no | all | `approvals`: only gates in this state | | `approval_id` | string (max 256) | for `decide` | — | the gate | | `decision` | enum `approve`, `reject` | for `decide` | — | only after the user decided | | `note` | string (max 65,536) | no | — | `decide`: kept with the decision | | `mode` | enum `supervised`, `tiered`, `autonomous` | for `mode` | — | who decides the gates | **Returns.** A heading line and the One API's response body as a JSON block, the same body as `structuredContent`. A refusal is `isError: true` text `<word>: <message> (<code>)` with `structuredContent.error = {status, code, message}`, the word standing for the REST status: `invalid` (400/422), `forbidden` (401/403), `not_found` (404), `conflict` (409), `unavailable` (5xx). `delete` of a running campus is `conflict` (stop it first). Details: [Companies](/mcp/companies). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_companies","arguments":{"user_request":"which companies do I have?","action":"list"}}}' ``` ## 14.22 plungeai_connections Your connected accounts, action for action the One API's `/v1/connections`: `list`, `add` (an API key for a key-based provider), `remove`, `link` (a signed single-use Studio link to connect a provider). `on_behalf_of` acts on one of your customers instead of you. - **Read-only:** no (`list` only reads; every list is written to the secrets audit log). **Output schema:** none; every answer also carries `structuredContent`. **Creates an execution:** no. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string (max 4,000) | no | — | the user's words | | `action` | enum `list`, `add`, `remove`, `link` | yes | — | what to do | | `provider` | string (max 64) | for `add`, `remove`, `link` | — | the provider id, e.g. `stripe`, `github`, `google-gmail` | | `on_behalf_of` | string (max 128) | no | you | one of your customers: 1–128 characters of `A-Z a-z 0-9 _ . -`, no `..` | | `api_key` | string (max 4,096) | for `add` | — | the provider's key; checked with the provider where the platform has a probe | | `api_secret` | string (max 4,096) | no | — | `add`: the secret of a key pair | | `return_url` | string (max 2,048) | no | — | `link`: the https URL the customer returns to | **Returns.** A heading line and the One API's response body as a JSON block, the same body as `structuredContent`: `list` `{connections: [{provider, on_behalf_of, method, status, connected_at}]}` (never key material), `add` `{provider, on_behalf_of, status: "connected"}`, `remove` `{…, status: "disconnected"}`, `link` `{url, expires_at}` (15 minutes, single use). Refusals use the same `<word>: <message> (<code>)` shape as `plungeai_companies`: `not_found` `unknown_provider` / `connection_not_found`, `invalid` `missing_api_key` / `credential_rejected` / `invalid_on_behalf_of` / `invalid_return_url`, `forbidden` `owner_not_found`, `unavailable` `connections_unavailable`. Details: [Connections](/mcp/connections). ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_connections","arguments":{"user_request":"what accounts have I connected?","action":"list"}}}' ``` # Troubleshooting & gotchas Source: https://www.plungeai.com/docs/mcp/troubleshooting Source: `orchestration/mcp-gateway/index.ts`, `auth-middleware.ts`, `server.ts`, `session-do.ts`, `tools.ts`, `tool-exec.ts`, `tool-outcome.ts`, `execution.ts`, `leg-state.ts`, `resources.ts`, `extras.ts`, `activity-log.ts`, `core/core-base/provider-ids.ts`, `core/agent-core/index.ts`, `agents/agents/one-agent/call-config.ts`, `orchestration/api-gateway/routes/mcp.ts`, live registry and registry cards (2026-09-24), live `/health`, `/v1/info`, 401/405/413 responses (2026-09-24). Most problems fall into one of three groups: the key, the transport, or which id you used. This chapter starts with five checks that settle most of them, then maps symptoms to causes, then lists the gotchas that come up in real use. Every row was checked against the code or a live response. The endpoint is `https://mcp.plungeai.com/v1`; `https://api.plungeai.com/v1/mcp` is the same server, and where it behaves differently the row says so. ## 15.1 Five first checks 1. **Is the platform up?** No key needed. `status` is `healthy`, or `degraded` with HTTP 503 when the database, key store or registry probe fails: ```bash curl -s "https://mcp.plungeai.com/health?cb=$(date +%s)" ``` 2. **Which server and protocol version am I talking to?** No key needed: `curl -s https://mcp.plungeai.com/v1/info` ([4.2](/mcp/protocol#42-endpoint-paths-headers-and-methods)). 3. **Does my key work, and whose is it?** Ask *"Who am I, what tier am I on, and how much of my rate limit have I used?"*, or call it directly: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I"}}}' \ | sed -n 's/^data: //p' ``` 4. **What did my client actually send?** Your activity log shows every request with its arguments, your own words (`user_request`), and the answer or error ([13.10](/mcp/errors-limits#1310-the-activity-log)). It is the fastest way to see that a client sent `{folder: "Finance"}` when you asked something else. 5. **Is the tool list stale?** Clients read `tools/list` once, when they connect, and this server never pushes a change notification. After a platform update, **restart or reconnect your client** to pick up new or changed tools. ## 15.2 Symptoms and fixes | Symptom | Cause | Fix | |---|---|---| | `401` on every call | The key is wrong, replaced, revoked or expired, or the header is wrong. The message is the same for all of these: `No valid authentication provided…`. A Bearer token that does not start with `ozk_` is never accepted. | Check the header (`Authorization: Bearer ozk_…` or `X-API-Key`). Mint a new key at **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`) and revoke the old one. | | `401 API key not permitted from <ip>`, or `401` only from a new place | The key is restricted to certain network locations | Use it from an allowed network, or ask the account owner who issued it to update the fence. | | `429 Rate limit exceeded: Rate limit: <n> requests/minute` (or `/day`) with `Retry-After` | Your tier's limit ([13.7](/mcp/errors-limits#137-rate-limits)) | Wait the `Retry-After` seconds. Ask *"how much of my rate limit have I used?"*. | | `429 Too many failed authentication attempts` | Many failed logins from your IP, often a client retrying with a bad key | Fix the key in the client config, then wait for the window to reset. | | `Tool not permitted for this key: <name>` | Your key is limited to a subset of tools | Use a key that allows the tool, or ask the account owner. | | Fewer tools than expected | A restricted key (fenced tools never appear in `tools/list`), or a stale client list | Check with `tools/list`. Reconnect the client. | | `Tool not found: <name>`, or your client doesn't know a tool | The client's tool list predates a platform update, or the name is misspelled | Reconnect to refresh the tool list. | | `405 Method not allowed.` on `GET` | Your client tried to open a standalone SSE stream. The server is stateless and offers none ([4.5](/mcp/protocol#45-stateless-by-default-opt-in-sessions)). | Nothing, for spec-compliant clients: they continue with `POST`. | | `404 Session not found` | The client sent an `Mcp-Session-Id` that is stale, expired (10 min idle) or from another key. Normal keys never get one. | Drop the header and re-initialize. | | `400 Parse error` | The body is not valid JSON, usually shell quoting | Check the quotes around `-d '…'`. Inside single quotes, write an apostrophe as `'"'"'`. | | `413 Request body too large (1 MiB max)` | Body over 1,048,576 bytes | Shrink it: save a long workflow first and run it by id, or split the work. | | `403 Invalid Host header` | A proxy between you and the server rewrote the `Host` header | Call `https://mcp.plungeai.com/v1` directly. | | curl prints `event: message` and `data: {…}` lines | `tools/call` answers as Server-Sent Events | Keep the `data:` lines: `… \| sed -n 's/^data: //p'` ([4.6](/mcp/protocol#46-toolscall-over-sse-progress-and-keepalive)). | | A long run returns an id instead of a result | Background mode: `mode:"async"`, or `plungeai_run_mission` / `plungeai_learn`, which default to it | Normal. Ask *"is that still running?"* and then *"show me the result"* ([Runs, results & follow-ups](/mcp/runs-results)). | | `This run failed: …` | The run genuinely failed, and that is the reason | Fix the input and rerun. Don't keep polling. | | A background run shows `Interrupted (run did not complete)` | The run stopped sending a heartbeat for 16 minutes and was marked failed ([4.7](/mcp/protocol#47-async-mode)) | Rerun it, run it in the foreground, or split it into smaller steps. | | `Run was killed 3 times in a row without yielding a checkpoint…` | One step ran longer than a 10-minute leg, crashed repeatedly, or kept getting interrupted by deploys | Split the work or shorten the step, then rerun. | | `…is not status:active` / `No such agent: <id>` (`🚫 UNAVAILABLE`) | The agent is not in the live catalog, or the id was guessed | Pick a listed alternative, or search: *"which agent should I use to …"*. | | `That is a scheduler run id, which this tool cannot resolve…` | An `exec_…` id from the scheduler | Ask for the job's run history (`plungeai_schedule` `runs`) and use its **Execution ID** column. | | `No result found for <id>, or it is not one of your executions.` | A workflow id where an execution id belongs, or another account's id | Ask *"show me my recent executions"* and use an id from there ([15.3](#153-gotchas-worth-knowing)). | | `Execution not found.` from a status check or `plungeai_executions` | No run of yours has that id: it is unknown, belongs to another account, or is a workflow id where an execution id belongs | Ask *"show me my recent executions"* and use an id from there. | | `Execution is completed, not running — nothing to cancel.` | Cancel only applies to running executions | Nothing to do. | | Your client shortens or paraphrases a long result | The client's own behaviour. The server's instructions ask it to relay results verbatim. | Add *"show it verbatim, don't summarize"*. | | `/health` answers `503` with `"status":"degraded"` | A platform dependency probe failed | Platform-side. Retry later. | Need a new key? Self-serve one at **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`). For a shared team key, or anything else, ask the account owner. ## 15.3 Gotchas worth knowing These come from real failures. **"Category" and "folder" are different things.** Your *folders* (Finance, Personal, …) hold **your workflows**. *Categories* are the global agent taxonomy, and the finance one is called `financials`, not `finance`. On 2026-09-24 the registry answered `category=finance` with 0 results and `category=financials` with `sec-agent` (live-verified). Say *"agents for financial analysis"* and let the semantic search work. Or read `plungeai://agents/categories` for the real names ([Prompts & resources](/mcp/prompts-resources)). **Results need the execution id, not the workflow id.** A workflow id identifies the recipe, and each run gets its own **execution id**. Every run result ends with its execution id. If a result lookup comes back empty, ask *"show me my recent executions"* first and use that id. When you refer to a row by number ("show me #2"), the server tells clients to resolve it against the list you were shown, not a fresh one, because new runs keep shifting a newest-first list. **Don't ask for "all agents".** An unfiltered listing is large: about 150 KB for the 92 active agents on 2026-09-24 (live-verified), in one go into your context. `plungeai://agents/list` is the same listing. Filter by search term, category or kind instead. Everything the catalog lists is live and callable. Names from outside it (memory, another platform, a guess) are refused with `🚫 UNAVAILABLE` before anything runs, so when in doubt, search first. **Some agents are forms, not chats.** Agents such as `markitdown`, `weather-agent` or `google-gmail` take specific fields (a URL, a place, a message id), not a sentence. Your client handles this itself: it fetches the agent's **contract** (every field typed, with a worked example) and fills it in before running ([Tools](/mcp/tools)). If something is still missing, the answer names the exact fields and shows the form. It never comes back as a bare error, so *"do what it suggests"* is always enough. A prose prompt sent to an agent with several operations and no `operation` runs its **default** operation, and the answer says so with a `⚠️ structured agent: …` warning. **Writing workflow YAML by hand? Quote your colons.** A value like `prompt: DD: memo` breaks YAML. Write `prompt: "DD: memo"`, or put long text in a block (`prompt: |`). Never hard-wrap a value across lines. On invalid YAML, `plungeai_execute_workflow` adds exactly this tip to its answer. The top-level `workflow:` wrapper is optional: bare `name:` + `tasks:` at the top level is accepted and wrapped for you. Easier still, describe the goal and let *"build me a workflow that…"* write the YAML ([Workflows](/mcp/workflows)). On a `one-agent` task you can leave `provider` out to get the default (`gemini`, model `gemini-3.1-flash-lite`). The provider resolver maps `google` to `gemini` (`core/core-base/provider-ids.ts`). Any other unknown provider is refused: `Unsupported provider: <x>`, or `provider "<x>" is not available on llm-agent` (call mode keeps the legacy wording) when the provider exists but the agent has no binding for it. **"Needs a connected account" means exactly that.** Google, Microsoft and similar agents act as *you*, so they need your account connected once in Studio → Connectors. The platform checks this **before** running anything. If the connection is missing or expired, you are told exactly which service to connect and where (`🔌 NEEDS CONNECTION`): no run is started, and nothing half-happens. The same goes for bring-your-own-key agents (wallets, private APIs, `🔑 NEEDS API KEY`): save your key once in Studio and every surface can use it. Ask *"what does this agent need, and am I connected?"* and the contract's **Credentials** section shows the live status: `✅ connected as you@…`, `⚠️ NOT connected`, `⚠️ expired — reconnect`, or `🔐 platform-managed`. **Broad questions about the platform's own structure can be slow.** A question like *"what are all the categories and how many agents in each?"* can make a client fetch agent cards one at a time. The `plungeai://agents/categories` resource answers it in one read. Otherwise ask a narrower question. **Background runs return immediately.** A long workflow answers with an execution id, not a result. That is normal. Ask for the result once it has finished ([4.7](/mcp/protocol#47-async-mode)). **A quiet client is not a stalled run.** If you see no live status while something takes a while, the run is still fine. Some clients do not display the progress channel at all. Two ways to tell: the result arrives normally when the call ends, and the run is in your execution history as `running` the whole time (*"is that still running?"*, or Studio's results sidebar). **Results are already formatted, so ask for them "in full".** Everything the server returns is finished Markdown (tables, headings, links), including a step index for multi-step runs and the whole conversation thread for runs you have followed up on. Chat clients sometimes summarize long answers anyway. Adding *"show it verbatim, don't summarize"* stops that. **Give financial agents a company name, ticker or CIK.** `sec-agent` resolves the company from what you pass, in the order CIK → ticker → name → prompt (its registry card, 2026-09-24). A bare fragment inside a long sentence resolves worst. *"Latest annual revenue for ticker CRCL"* or *"Apple insider trading Form 4"* work. **Data providers run out.** An agent whose upstream plan hit its quota reports that itself. You get `🚫 UNAVAILABLE` with the agent's own reason, not a broken-looking error ([13.2](/mcp/errors-limits#132-the-structured-feedback-contract)). Retry later, or use another agent that reads the same data (for company figures, filings via `sec-agent`). **Claude Desktop caps a single tool call at about 4 minutes.** For anything longer, ask for it to run "in the background". You get an execution id at once, and you can ask for its status or result at any time. The server sends a keepalive line every 15 seconds, but that does not extend a client's own cap. **Always let your client pass your words.** Every tool takes `user_request`, the sentence you actually typed. It shows up at the top of each entry in your activity log (`> **Asked:** …`), which turns a puzzling call into an obvious one when you debug. # Companies Source: https://www.plungeai.com/docs/mcp/companies Source: `orchestration/mcp-gateway/tools-companies.ts` (`CompaniesSchema`, `plungeaiCompanies`), `orchestration/mcp-gateway/ops/companies.ts` (`decideGate`), `orchestration/mcp-gateway/shared.ts` (`refusalResult`, `valueResult`), `orchestration/company-service/contract.ts` (`CompanyServiceApi`, the request and response types), `orchestration/api-gateway/routes/companies.ts` (the REST twin). A **company** is a campus compiled from documents you saved as workflows: one `kind: campus` document, its `kind: building` documents and their offices. `plungeai_companies` is the MCP door to it, action for action the One API's `/v1/companies`: the same company-service, called as you, with the same bodies and the same refusals. Full parameter table: [14.21](/mcp/tool-reference#1421-plungeai_companies). ## 16.1 Tenancy The key's user is the tenant. Every action but `list` takes `campus` (the slug of a campus you saved), and company-service only finds campuses you own: a campus another account owns is the same `not_found` as one that does not exist, so the answer never tells you that a slug is taken. A slug that cannot be a campus slug (lowercase letters, digits and dashes, 1–63 characters) is `not_found` without a call. ## 16.2 Actions | `action` | Needs | REST twin | Does | |---|---|---|---| | `list` | nothing | `GET /v1/companies` | your campuses: slug, name, mode, status, buildings, pending gates | | `apply` | `campus` | `POST /v1/companies/{campus}/apply` | compiles the saved documents and stores the plan; idempotent by name | | `layout` | `campus` | `GET …/layout` | the compiled campus, buildings and offices | | `snapshot` | `campus` | `GET …/snapshot` | live state: offices, items, the queue of pending gates, spend today | | `events` | `campus`; optional `since` (default 0), `limit` (default 200, max 1,000) | `GET …/events` (JSON page) | the event log after `since`, oldest first. The server-sent stream is REST only | | `ingest` | `campus`, `events` (1–200) | `POST …/events` | a local building reports its events up | | `slice` | `campus`, `building` | `GET …/buildings/{building}/slice` | the part of the plan one building runs | | `item_create` | `campus`, `building`, `title`; optional `body`, `kind`, `priority`, `tier` | `POST …/items` | puts an item through the building | | `item_pause`, `item_resume`, `item_retry`, `item_cancel` | `campus`, `item_id` | `POST …/items/{id}/{verb}` | moves one item | | `item_steer` | `campus`, `item_id`, `text` | `POST …/items/{id}/steer` | a message for the item's running office | | `approvals` | `campus`; optional `status` | `GET …/approvals` | the gates at the door | | `decide` | `campus`, `approval_id`, `decision`; optional `note` | `POST …/approvals/{ap}` | records the decision; see 16.3 | | `mode` | `campus`, `mode` | `PUT …/mode` | `supervised`, `tiered` or `autonomous`: who decides the gates | | `start`, `stop` | `campus` | `POST …/start`, `POST …/stop` | arms or stops the campus clock | | `delete` | `campus` | `DELETE /v1/companies/{campus}` | deletes the campus and every row of it; the saved documents stay, so it can be applied again | ## 16.3 Deciding a gate Only send `decide` after the user decided. For a `tool_permission` gate, `approve` continues the execution that paused for it, once, as you, and hands the result back to the company so the item moves on: the answer carries `execution_id` and `continued: true`. A continue that cannot run is reported beside the recorded decision (`continued: false` with `continue_error`: `not_paused`, `session_expired`, `execution_not_found` or `continue_failed`), never raised. `reject` leaves the run paused: nothing is continued. ## 16.4 Answers and refusals An answer is a heading line (for example `` Created item `<id>` on `<campus>`. ``) and the REST response body as a JSON block; the same body is `structuredContent`. A refusal is `isError: true` text `<word>: <message> (<code>)`, and `structuredContent.error` is `{status, code, message}`: | `status` word | REST status | Typical codes | |---|---|---| | `invalid` | 400, 422 | `invalid_params` (a missing or malformed argument; no call is made) | | `not_found` | 404 | `company_not_found`, `building_not_found`, `item_not_found`, `approval_not_found` | | `conflict` | 409 | `conflict` (`delete` while the campus runs — stop it first), `item_done`, `approval_not_pending` | | `unavailable` | 5xx | `not_enabled` (no company service on this deployment), `upstream_error` (the call failed; retry) | Try asking: - *"Which companies do I have?"* → `list` - *"Add an item to the docs building: write the onboarding guide."* → `item_create` - *"What is waiting for my approval at plunge?"* → `approvals`, then `decide` once the user says so - *"Shut plunge down and delete it."* → `stop`, then `delete` List your companies (read-only): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_companies","arguments":{"user_request":"which companies do I have?","action":"list"}}}' ``` Put an item through a building. This changes state (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_companies","arguments":{"user_request":"have the docs building write the onboarding guide","action":"item_create","campus":"<campus slug>","building":"<building slug>","title":"Write the onboarding guide","kind":"docs"}}}' ``` # Connections Source: https://www.plungeai.com/docs/mcp/connections Source: `orchestration/mcp-gateway/tools-connections.ts` (`ConnectionsSchema`, `plungeaiConnections`), `orchestration/mcp-gateway/ops/connections.ts` (the store, the subject rule, the connect link), `core/core-secrets/UserCredentialsService.ts`, `core/core-secrets/connect-token.ts`, `orchestration/api-gateway/routes/connections.ts` (the REST twin). `plungeai_connections` manages the accounts your agents act through, action for action the One API's `/v1/connections`. Credentials are stored encrypted on the compliance database and never come back out: a list shows provider, method, status and when, nothing else. Full parameter table: [14.22](/mcp/tool-reference#1422-plungeai_connections). ## 17.1 Whose account: `on_behalf_of` Without `on_behalf_of` a call acts on your own account. With it, it acts on one of **your customers**: the id is yours to choose (1–128 characters of `A-Z a-z 0-9 _ . -`, no `..`), and the row is kept under your account, so no other key can reach it. An empty or malformed value is refused (`invalid_on_behalf_of`), never read as "me". ## 17.2 Actions | `action` | Needs | REST twin | Answer (`structuredContent`) | |---|---|---|---| | `list` | optional `on_behalf_of` | `GET /v1/connections` | `{connections: [{provider, on_behalf_of, method, status, connected_at}]}`. Every list is written to the secrets audit log | | `add` | `provider`, `api_key`; optional `api_secret`, `on_behalf_of` | `POST /v1/connections/{provider}` | `{provider, on_behalf_of, status: "connected"}`. Where the platform has a probe for the provider, the key is checked first; a key the provider rejects is `credential_rejected` | | `remove` | `provider`; optional `on_behalf_of` | `DELETE /v1/connections/{provider}` | `{provider, on_behalf_of, status: "disconnected"}`; nothing to remove is `connection_not_found` | | `link` | `provider`; optional `return_url` (https), `on_behalf_of` | `POST /v1/connections/{provider}/link` | `{url, expires_at}`: a signed Studio link, single use, valid 15 minutes. Send it to whoever owns the account; they connect there (OAuth or key) and return to `return_url` | `provider` must be a provider the platform can connect: a key-based one (`stripe`, `openai`, `apollo`, `custom`, …), an OAuth one (`github`, `google-gmail`, …) or one an agent's connection prompt names. Anything else is `not_found` `unknown_provider`. The key and secret are redacted from the activity log like every other secret-named argument. ## 17.3 Refusals The same shape as [16.4](/mcp/companies#164-answers-and-refusals): `isError: true` text `<word>: <message> (<code>)` and `structuredContent.error = {status, code, message}`. | `status` word | Codes | |---|---| | `invalid` | `invalid_params` (no `provider`), `missing_api_key`, `credential_rejected`, `invalid_on_behalf_of`, `invalid_return_url` | | `forbidden` | `owner_not_found` (`link` from a key whose user account is gone) | | `not_found` | `unknown_provider`, `connection_not_found` | | `unavailable` | `connections_unavailable` (the credential store is not reachable from this deployment) | Try asking: - *"What accounts have I connected?"* → `list` - *"Send my customer acme-42 a link to connect their GitHub."* → `link` with `on_behalf_of: "acme-42"` - *"Disconnect Stripe."* → `remove` List your connections (read-only): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_connections","arguments":{"user_request":"what accounts have I connected?","action":"list"}}}' ``` Make a connect link for a customer. This signs a link (not verified here): ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_connections","arguments":{"user_request":"send acme-42 a link to connect GitHub","action":"link","provider":"github","on_behalf_of":"acme-42","return_url":"https://app.example.com/connected"}}}' ``` # OceanCode guide Source: https://www.plungeai.com/docs/oceancode `oceancode` is one binary and one process. This page builds it, shows the two ways to start it, and lists where it keeps its files. ## Build ```bash cargo build --release -p ocean-cli ``` The binary is named `oceancode` and, because the workspace builds into a shared target directory, it lands at the repository-root `target/release/oceancode` (that is `../../target/release/oceancode` from this app directory). Put it on your `PATH` or call it by that path. ## The three boot shapes `oceancode` starts in one of three shapes, chosen by the subcommand. | Shape | Command | What runs | |---|---|---| | Pager | `oceancode` | Ocean pager (`ocean-pager`) with the Ocean engine in the same process over ACP — the terminal UI. See [19 · Ocean pager](/oceancode/pager). | | Editor | `oceancode acp` | The engine as an Agent Client Protocol agent on stdin/stdout — what Xcode and other ACP editors launch as a subprocess. See [20 · Xcode and other ACP editors](/oceancode/xcode). | | Headless | `oceancode serve`, or a non-interactive subcommand (`run`, `goal`, `workflow`, `ledger`, `export`, …) | The engine only, no UI: `serve` exposes the HTTP surface, the others run one job and exit. | ```bash # 1. Pager + engine (the usual way) oceancode # 2. Headless server on port 4096 oceancode serve # 3. One headless run oceancode run "summarise the README" ``` The default mode accepts `--db`, `--directory`, `--listen` and `--verbose` (`-v`) and passes them to the engine. `--tui pager` is accepted for compatibility and changes nothing; `--tui ocean` fails with "the native TUI was removed; `oceancode` starts Ocean pager". Binding beyond loopback needs `--listen` and a server password; see [07 · Headless and server](/oceancode/headless-and-server). ## First run Authenticate once, then start a session. ```bash oceancode auth # interactive: API key or Claude Pro/Max subscription # or, non-interactively: oceancode login # Claude Pro/Max subscription login oceancode key <key> # store an Anthropic API key ``` The credential is written to `auth.json` in the data directory (below) and reused on every later run. Then launch `oceancode` and type at the prompt. `oceancode key <provider> <key>` stores another provider's key in the same file — `oceancode key typesafe <key>` stores a TypeSafe key, which turns on the Jev decision layer ([13 · Jev decisions](/oceancode/jev-decisions)); `oceancode key` alone reports what is stored. ## Starting a session A plain `oceancode` opens the pager on a fresh session. Earlier sessions are listed and resumed from inside the pager — see its own user guide under `ocean-pager/crates/ocean-pager-ui/docs/user-guide/`. Headless runs pick a session with `oceancode run --continue` / `--session <id>`; see [07 · Headless and server](/oceancode/headless-and-server). ## Updating and removing Keep the binary current, verify a download, or remove it: ```bash oceancode update # update to the newest release for your channel (verified, atomic, rollback on failure) oceancode verify-release oceancode-macos-aarch64.tar.gz # check a download's signature before trusting it oceancode uninstall # remove the binary (add --purge to also remove config + data) ``` See [17 · Distribution and updates](/oceancode/distribution) for install one-liners, channels, and the signature/checksum trust model. ## Where files live Paths follow the XDG layout on every platform (macOS included — `~/.config`, not `~/Library`). `<x>` below is the matching `$XDG_*_HOME` when set, otherwise the home-relative default shown. | What | Location | |---|---| | Global config | `$OC_CONFIG`, else `<XDG_CONFIG_HOME \| ~/.config>/oceancode/oceancode.json` | | Credentials | `<XDG_DATA_HOME \| ~/.local/share>/oceancode/` — `auth.json` | | Models catalog cache | `<XDG_CACHE_HOME \| ~/.cache>/oceancode/models.json` | | Session database | `oceancode.db` in the working directory, unless `--db <path>` is given | | Project config and assets | `<project>/.oceancode/` — `oceancode.jsonc`, `oceancode.json`, `skills/`, `plans/` | ## Next - [02 · Configuration](/oceancode/configuration) — the config keys and environment variables. - [19 · Ocean pager](/oceancode/pager) — the terminal UI; its slash commands and keys are in `ocean-pager/crates/ocean-pager-ui/docs/user-guide/`. # Configuration Source: https://www.plungeai.com/docs/oceancode/configuration `oceancode` reads a JSON config file and a set of environment variables. Nothing needs to be configured to start — every key falls back to a built-in default. ## Config file locations and precedence There are two config files, and both are optional. | Scope | Path | Notes | |---|---|---| | Global | `$OC_CONFIG`, else `<config-dir>/oceancode/oceancode.json` | One file for every project you run. | | Project | `<project>/.oceancode/oceancode.jsonc`, then `<project>/.oceancode/oceancode.json` | Per-repository, kept next to the code. | `<config-dir>` is `$XDG_CONFIG_HOME` when set, otherwise `~/.config` — on every platform, macOS included (the binary uses the XDG layout everywhere, not `~/Library`). **Precedence, lowest to highest:** built-in default → project file → global file → the managed file ([12](/oceancode/permissions-and-sandbox#managed-enterprise-config)) (`crates/ocean-app/src/services.rs:3000`). The global file is layered over the project file, so a key set in both takes the global value; the `jev` section is the one exception, read per file ([13](/oceancode/jev-decisions#modes)). Within the project directory, `oceancode.json` is merged over `oceancode.jsonc` when both exist. Merging is per section, last writer wins. Comments and trailing commas are allowed in the `.jsonc` file. ## Top-level keys Every key is optional; an empty or absent value takes the built-in default. Unknown keys are ignored, so a file that still carries the native-TUI keys removed on 2026-10-05 (`tui`, `keybindings`, `disabled_providers`, `notifications`) loads unchanged. | Key | Type | Default | Meaning | |---|---|---|---| | `$schema` | string | none | JSON-schema URL, for editor completion only. | | `model` | string | provider default | Default model, as `provider/model`. | | `effort` | string | none | Reasoning effort a new session's `model` runs at, as the model's own level name (`xhigh`, `high`, `on`, `off`, `minimal`, …; `/effort` lists the levels the current model has). Any level without whitespace loads; one the model does not list sends no effort field. Wins over `~/.claude/settings.json`'s effort when both name the same model. An empty value never overrides an earlier layer. | | `default_agent` | string | `build` | Agent a new session starts in. | | `mode` | string | `model` | The work mode: a diagram id (lowercase letters, digits and dashes, starting with a letter), such as `delegate` or `race`. `model` lets the model decide, as before work modes. An id no diagram has runs `model`, with a log line. A session's own pick (`/mode`, `F4`) and `agents.<id>.mode` come first. See [23 · Work modes](/oceancode/work-modes). | | `test_command` | string | none | The project's test command, run by a work mode's `tests` gate that names no command (Race needs it). It runs through the `bash` permission rules. | | `agents` | object | none | Per-agent overrides, keyed by agent name. Each agent may set `model`, `prompt`, `temperature`, `disable`, `tool_profile` (`default` / `codex` — `apply_patch` is the sole mutation tool / `hashline` — the content-anchored `hashline` tool replaces `edit`), and `effort`. `model` (`provider/model`) and `effort` (a level name) are the agent's defaults: when a session switches to the agent (Shift+Tab, or a new session started in it) it takes that model, when the catalog lists it and its provider is connected, and that effort, clamped to the levels the model offers (the highest offered level at or below it, else the lowest; none on a model without levels). An empty field keeps the session's own. An agent that sets both `model` and `effort` also makes a `task` call naming that model (`model: "provider/id"`) run at that effort, over a `modelSettings` row in `~/.claude/settings.json` for the same model. The pager edits the rows for `build`, `plan`, `ask` and `debug` under F2 › Agent defaults ([22](/oceancode/pager-reference)). | | `agents.<id>.mode` | string | none | The work mode a session of that agent runs when the session picked none itself; the picker's `d` writes it. This is a diagram id, not the `mode:` line of an agent's markdown file (`primary` / `subagent` / `all`). See [23](/oceancode/work-modes). | | `providers` | object | none | Per-provider settings (endpoints, options). | | `formatter` | bool or object | built-ins on | Code formatters: `false`/`true` toggles the built-ins; an object overrides them per formatter. | | `instructions` | array of strings | none | Extra system-prompt sources — each a file path (`~`- or working-dir-relative) or an `http(s)` URL. | | `mcp` | object | none | MCP servers, keyed by name. | | `ocean` | object | none | Ocean Platform integration — see [15 · Platform mode](/oceancode/platform-mode). | | `permission` | array | `* * allow` | Base permission rules (`allow` / `ask` / `deny`); last matching rule wins. An empty config allows every call except the shell's dangerous floor, an `external_directory` ask and denied `.env` reads; add `bash * ask` to make every shell command off the read-only safe list ask (the posture Jev's permission point advises on, [13](/oceancode/jev-decisions)). | | `permission_mode` | string | ask | Default permission mode applied at boot: `acceptEdits`, `dontAsk`, `bypassPermissions`, or empty to ask on every side effect. An empty value never overrides an earlier layer. See [12 · Permissions and sandbox](/oceancode/permissions-and-sandbox). | | `shell` | object | built-in guard | Shell tool-guard overrides: `safe_list_extra` (extra read-only commands), `allow` / `deny` (`Tool(pattern)` rules layered on the classifier); plus `kind` (`auto` / `bash` / `sh` / `powershell` / `cmd`, default `auto`) — the shell the `bash` tool spawns, mainly for Windows (`auto` = `sh` on Unix, PowerShell on Windows). See [12](/oceancode/permissions-and-sandbox#the-shell-guard) and [17 · Distribution and updates](/oceancode/distribution). | | `child_env` | object | strip secrets | How a spawned child's environment is composed: `inherit` (`all` / `core` / `none`), `include_only`, `set` (name→value), `strip_secrets`. See [12](/oceancode/permissions-and-sandbox#child-process-environment). | | `sandbox` | object | off | OS isolation profile: `profile` (`off` / `workspace` / `readonly` / `strict`), `deny` (roots refused even inside the workspace), `network` (`off` / `allowlist`), `allow_hosts`. See [12](/oceancode/permissions-and-sandbox#the-sandbox). | | `refs` | object | none | Project-reference aliases: `refs` maps `@name` → a path; a `@name/…` path in any file tool expands to it and its root is auto-allowed through the external-directory prompt. See [12](/oceancode/permissions-and-sandbox#reference-aliases). | | `tool_output` | object | built-in thresholds | Overrides for tool-output truncation limits (built in: 2,000 lines / 50 KiB). A result over them reaches the model as a head preview with the full output saved to a spill file it can `read`; this applies to every tool that does not page itself, `bash` included. | | `claude` | object | none | Claude Code (Pro/Max subscription) transport settings. | | `ledger` | object | on, tideline = 40% of the model's context, block = 70% of the line | Session Ledger settings, under `ledger.tideline` (the aliases `ledger.shelve` and `ledger.window` still parse): `tideline.percent` (the tideline — the **line** — as a **share of the model's context**, `1..=100`, default 40 — while the window's real-token gauge is at or over it, the oldest closed block leaves the window); `tideline.block` (how much a block holds before it closes: a token count used verbatim (`70000`), or a **share of the line** as a string (`"35%"`, `1..=100`) that is re-derived whenever `/tideline` or the route changes the line — the rule "cut 35 % per cut" stays 35 % on every model; unset = 70% of the line; `0`, `"0%"`, `"101%"` and a string without `%` are refused at load); `tideline.tokens` (the fixed-token line used only when the model's context is unknown, default 300k; `0` = the model's own ceiling). The keys of the old band geometry — `low`, `reserve`, `profile` under the tideline block and `ledger.hard_tokens` — are removed: an old config still loads, they are ignored, and the load logs one warning naming them. `ledger.model` — the merge/lint model, default the session's (empty): every merge and lint call is a separate full-price request that never touches the session's cache prefix, so an empty value bills it at the session model's price — set a cheaper one (`provider/model`). Examples `{"ledger":{"tideline":{"percent":50}}}`, `{"ledger":{"tideline":{"percent":20,"block":"35%"}}}` (a 200 K line on a 1 M model, 70 K per cut), `{"ledger":{"tideline":{"tokens":500000,"block":20000}}}` or `{"ledger":{"model":"openai/gpt-5-mini"}}`. See [06 · Session ledger and export](/oceancode/session-ledger-and-export). | | `retention.days` | number | `7` (`0..=3650`) | How many days an idle session is kept: past it the session (with its whole sub-agent tree) is deleted from `oceancode.db` and its Session Ledger directory goes with it. `0` = off. One setting for the database and the ledger; nothing is deleted for 24 hours after the sweep is first armed. `/retention` reads and sets it. See [18 · Operations](/oceancode/operations#retention-and-vacuum). | | `database.retention_days` | number | none | Legacy fallback: read only when no config layer has a `retention` section, and only when > 0. (`database.vacuum` is no longer read.) | | `ledger.retention_days` | number | none | Ignored since `retention.days`; a config that still sets it loads with one warning at boot. | | `compress` | string | `safe` | Prompt compression of tool results, `safe` (the default) or `off`. `safe` runs five lossless stages (ANSI strip → fold of identical lines → the session directory written `./` → a result identical to an earlier one becomes `[same output as <tool-use id>]` → a run of lines found in an earlier result becomes `[↑<n> lines same as <tool-use id>:L<a>-L<b>]`), on the copy of the request sent to the provider only — the stored session is never changed and the tideline cuts on the same turns to within the token estimate, and never earlier. Files and images are never touched. Overridden by `OC_COMPRESS`; an unknown value is read as the default and logs one warning. | | `subagent` | object | none | Default native sub-agent model as `subagent.model` = `provider/model` (e.g. `{"subagent":{"model":"openai/gpt-5-mini"}}`). The model a session's sub-agent children run when the session sets no `/subagent` override and the fleet router pins none (`env.CLAUDE_CODE_SUBAGENT_MODEL`, applied while the subscription slot is live); empty (unset) leaves children on the spawning session's own model. A value without a `/` is rejected. Since 2026-10-21 a set `subagent.model` is the default ahead of the fleet router's pin. | | `subagent.effort` | string | none | Reasoning effort sub-agents run at — the model's own level name, as for `effort`. Applies to `subagent.model`, or to the router's sub-agent model when `subagent.model` is unset; when both name the same model and this is unset, the `~/.claude/settings.json` effort is kept. | | `subagent.force` | bool | `false` | `true` makes the `task` tool ignore a per-call `model:` and run the sub-agent default — the same as `CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1` in `~/.claude/settings.json` (either one forces). | | `subagent.max_concurrent` | number | none (`0..=64`) | Sub-agents that may run at once in a session's whole tree. Unset: the work mode's own limit, else none. A set value wins in every mode, Model included; `0` means no sub-agents. See [23](/oceancode/work-modes#limits). | | `subagent.max_per_turn` | number | none (`1..=1000`) | Sub-agents one turn may start before you are asked whether to go on. Unset: the work mode's own limit, else none. | | `subagent.max_depth` | number | `4` (`1..=4`) | How deep sub-agents may nest. Unset: the work mode's own limit, else 4. | | `workflows.enabled` | bool | `true` | Whether the host-run workflow conductor and goal mode are available at all. `false` turns the conductor off for the session. | | `workflows.max_concurrent_agents` | number | `8` (`1..=1024`) | Concurrency ceiling: how many agents a single workflow run may execute at once. A value outside `1..=1024` is rejected at load. | | `workflows.agent_budget` | number | `128` (`1..=1024`) | Total number of agents a single run may spawn before it stops. A value outside `1..=1024` is rejected at load. | | `workflows.max_run_minutes` | number | `120` | Wall-clock cap on a single run, in minutes. | | `workflows.agent_timeout_minutes` | number | `20` (`0` = off, else `1..=1440`) | Per-child await bound: how long the host waits on one sub-agent (a single `agent()` call) before it cancels that child alone and returns it to the script as `success:false` with `stop_reason: "timeout"` — the run and its other agents keep going. `0` disables the bound (only the run's `max_run_minutes` then applies). A non-zero value outside `1..=1440` is rejected at load. | | `workflows.orchestrate` | string | `off` | `off` \| `always`. `always` wraps every prompt in the `goal` workflow (goal mode): the prompt launches a background goal run and the turn returns at once, with the run's outcome arriving later as a workflow result in the session; `off` runs each prompt as a plain turn. This is a config key only — Claude Code's `ultracode` setting does NOT switch it on. Since work modes, `always` with no `mode` set is read as `mode: "goal"` ([23](/oceancode/work-modes)); a set `mode` wins. | | `skills.mode` | string | `all` | Skill discovery mode: `all` (every discovered skill is a candidate) or `allowlist` (only skills whose id is in `skills.enabled` are candidates; every other skill is disabled and costs zero prompt tokens). This is the **project tier** — the per-skill *user* toggle lives in `~/.oceancode/skill-state.json` (`oceancode skill enable/disable`). See [09 · Capabilities](/oceancode/capabilities#enabling-and-disabling-skills). | | `skills.disabled` | array of strings | none | Qualified skill ids the **project** turns off. A project can disable a skill but can never re-enable one the user disabled — the user tier is resolved first. | | `skills.enabled` | array of strings | none | The allowlist consulted only when `skills.mode = "allowlist"`: the skill ids that stay enabled — each a stable `<tier>.<eco>:<name>` id (see [09 · Capabilities](/oceancode/capabilities#enabling-and-disabling-skills)) or a bare name (matching every copy of that name). | | `skills.roster` | string | `names` | How the system prompt lists skills: `names` (one line of names, no descriptions — the default since 2026-10-21) or `full` (an `<available_skills>` element with name, description and location per skill — the default before). The skill tool loads any enabled skill by name either way. | | `jev` | object | on with a key | The TypeSafe Jev decision layer, on whenever a TypeSafe key resolves (`oceancode key typesafe <key>`, else `OC_TYPESAFE_API_KEY`, else `TYPESAFE_API_KEY`): `enabled` (an opt-out — `false` in either the global or the project config turns it off), `endpoint` (default `https://api.typesafe.ai/v1/systemone`), `model` (versioned id), budgets, one mode per decision point (`permissions`, `output_view`, `supervisor`, `triage`, `visibility`, `check`, `guidance`, `snippets`, `effort`, `work_mode` = `off`/`shadow`/`act`; unset → the point's default: `act` for `check`, `guidance`, `snippets`, `effort`, `work_mode` (asked only under the Auto work mode), `off` for the other five), `supervisor_max_continuations`, `prewarm`, `thresholds` (including `check_pass`, `check_remaining`, `supervisor_no_progress`, `guidance_yes`, `guidance_no`, `effort_confidence`, `work_mode_confidence`). A project config may set only `enabled` and the modes. **Deprecated, accepted with one warning and never read:** `tools` (alias `tool_tiers`) and `relevance` (points retired 2026-10-21), `supervisor_every_steps`, and the thresholds `tools_confidence`, `tools_min_p`, `relevance_needs`, `relevance_min_p`, `relevance_rerank`, `permission_scope`, `supervisor_looping`, `supervisor_loop_progress`. See [13 · Jev decisions](/oceancode/jev-decisions). | | `update` | object | auto-check on, `stable` | Self-updater settings: `auto_check` (bool, default `true`) checks for a newer release on boot; `channel` (`stable` / `beta`, default `stable`) selects release visibility; `endpoint` (string, unset → GitHub Releases) overrides the release-metadata source. Overridable per run by `OC_NO_UPDATE_CHECK` / `OC_UPDATE_CHANNEL`. See [17 · Distribution and updates](/oceancode/distribution). | | `queue` | object | `default_delivery: queue` | Prompt-queue defaults: `default_delivery` (`queue` — after the live turn, or `steer` — into the live turn) is how a prompt with no explicit delivery is admitted. | | `jobs` | object | `max_concurrent: 4`, `spill_max_bytes: 5242880` | Background-job limits for the `bash background:true` / `monitor` / `loop` / `scheduler` tools: `max_concurrent` running jobs and `spill_max_bytes` per-job output-spill cap. There is no job timeout: a job runs until it ends or `job_kill`, unless the run has a wall clock (`OC_MAX_MINUTES`), which reaps it. | | `checkpoints` | object | `enabled: false` | Whole-tree snapshots: `enabled` takes one before every `edit`/`write`/`apply_patch`/`bash` call for the HTTP `/session/{id}/undo`·`/redo` routes (about 1.8 s per call on a 40k-file project; the user's own `.git` is never touched). `/rewind` does not need it: the file tools keep a copy of each file before they change it. | | `context` | object | `durable_rules: true` | Context-epoch options: `durable_rules` injects each entered directory's rule file once as a durable message so it survives context compaction. | | `model_fallback` | array of strings | none | Ordered `provider/model` routes tried, in order, after the primary model fails. Paired with `fallback`. | | `fallback` | object | `cooldown_seconds: 60` | Provider-fallback options: `cooldown_seconds` is how long a `model_fallback` route is skipped after it fails. | ## Environment variables Set these in the shell before launching. A slash command or an `oceancode run` flag, where one exists, overrides the environment for that session (see [08 · Bounding a long run](/oceancode/bounding-a-long-run)). | Variable | Meaning | Default | |---|---|---| | `OC_CONFIG` | Absolute path to the global config file, overriding its default location. | unset → `<config-dir>/oceancode/oceancode.json` | | `OC_CONFIG_DIR` | Directory that holds the project `oceancode.jsonc` / `oceancode.json`. When set (non-blank), the project config is read from `$OC_CONFIG_DIR` instead of `<cwd>/.oceancode/`, so config can live outside the repo — a read-only checkout, or a benchmark board that grades the whole workspace tree (B6). Only the project tier moves; the user and managed tiers are unaffected. | unset → `<cwd>/.oceancode/` | | `OC_MANAGED_CONFIG` | Absolute path to the managed (admin/enterprise) config file, overriding its OS default. It is the highest-precedence layer, so it overrides the project and global files. See [12 · Permissions and sandbox](/oceancode/permissions-and-sandbox#managed-enterprise-config). | unset → macOS `/Library/Application Support/oceancode/managed_config.json`, Linux `/etc/oceancode/managed_config.json` | | `OC_REQUIREMENTS_PUBKEY` | Trusted ed25519 public key (64 hex chars) that a managed `requirements.json` signature is verified against; an unsigned or tampered file is refused, never applied. | unset → no key; managed requirements are not enforced | | `OC_SERVER_USERNAME` | Basic-auth username the HTTP server accepts. | `oceancode` | | `OC_SERVER_PASSWORD` | Basic-auth password. Empty disables auth; a non-loopback bind refuses to start without it. | unset (auth off) | | `OC_MODELS_URL` | Override URL for the models catalog. | the built-in models.dev URL | | `OC_MODELS_PATH` | Path to the local models-catalog cache file. | a file under the cache dir | | `OC_DISABLE_MODELS_FETCH` | Any non-empty value skips fetching the remote models catalog (offline). | unset (fetch on) | | `OC_NO_UPDATE_CHECK` | Any non-empty value suppresses the boot-time check for a newer release. The check is also skipped on first run and when `update.auto_check` is `false`; the first launch is telemetry-free regardless. See [17 · Distribution and updates](/oceancode/distribution). | unset (check on) | | `OC_UPDATE_CHANNEL` | Force the release channel for `oceancode update`: `stable` or `beta`. Overrides the `update.channel` config key for that run. | unset → `update.channel` (default `stable`) | | `OC_INSTALL_DIR` | Install / uninstall destination directory for the installers and `oceancode uninstall`. | unset → `$HOME/.local/bin` (Windows `%USERPROFILE%\.oceancode\bin`) | | `OC_MINISIGN_SECRET_KEY` | Release lane only — path to the minisign secret-key file `scripts/sign/sign-artifacts.sh` signs artifacts with. Read by the release scripts, never by the running binary; never committed. | unset | | `OC_WINDOWS_CERT_THUMBPRINT` | Release lane only — Authenticode code-signing certificate thumbprint `scripts/sign/authenticode-windows.ps1` uses. Read by the release scripts, never by the running binary. | unset | | `OC_TUI` | Compatibility only. `pager` is accepted and changes nothing (`grok` is refused as unknown) — `oceancode` always starts Ocean pager; `ocean` fails with "the native TUI was removed; `oceancode` starts Ocean pager". `--tui` takes the same values. | unset → Ocean pager | | `OC_PAGER_BIN` | Absolute path to the Ocean pager binary (`ocean-pager`) that `oceancode` execs. | unset → beside `oceancode`, then `PATH` | | `OC_PAGER_HOME` | Set by `oceancode` for the pager it execs: the pager's state directory. Read by `ocean-pager`, not the engine; every pager variable is in [19 · Ocean pager](/oceancode/pager#environment). | `<data>/oceancode/pager` | | `OC_RG_BIN` | Absolute path to a ripgrep (`rg`) binary. The `bash` tool rewrites a plain recursive `grep -r …` to `rg` and puts a directory holding that binary as `rg` first on the child `PATH` (a symlink/copy shim under `<data dir>/bin/` when the file is named otherwise). The pager passes its embedded ripgrep automatically; set this only to override it or on a machine without one (Windows). | unset → the pager's embedded `rg`, then `rg` beside `oceancode`, then `rg` on `PATH` | | `OC_MAX_MINUTES` | Wall-clock bound for a run, in minutes. Background jobs the run starts (`bash background:true`, `monitor`, `loop`, `scheduler`) are reaped with it; unset, they run until they finish or `job_kill`. | unset (no clock bound) | | `OC_MAX_USD` | Spend bound for a run, in US dollars. | unset (no budget bound) | | `OC_MAX_STEPS` | Per-turn step cap at the root, `1`–`1000`. A turn boundary the run auto-continues past, not a stop. | `300` | | `OC_MAX_SUBAGENT_STEPS` | Per-turn step cap for a sub-agent. Sub-agents stop at it. | `300` | | `OC_PLAN` | `off` disables the planning discipline for the run: no todo directive in the system prompt on either route, no plan-missing reminder, and a settled turn with open todos is not worked off. The PLAN panel, `/plan` and any todo block the model writes keep working. See [05 · Plan mode and todos](/oceancode/plan-mode-and-todos). | unset (on) | | `OC_COMPRESS` | Prompt compression profile for the run, `safe` or `off`; overrides the `compress` config key. An unknown value is read as the default and logs one warning. | unset → the `compress` key (default `safe`) | | `OC_READ_STEER` | By default `cat <paths>`, `head [-n N]`, `tail [-n N]` and `sed -n '<range>p'` on files that exist under the command's working directory are answered by the `read` tool without starting a shell: line numbers, paging, truncation guards, the `.env` deny and freshness tracking apply exactly as for a direct `read`. `refuse` restores the old refusal (the error names the exact `read` call by absolute path); `off` runs pure reads in the shell as typed. Pipes, redirects, chains, globs, missing paths, `tail -f` and `sed -i` always run. POSIX shells only. | unset (serve) | | `OC_MAX_OUTPUT_TOKENS` | Overrides the model's per-request output-token cap, clamped to the model's own limit. | the model's published maximum | | `OC_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | `0` or `false` disables background sub-agents (the `task` tool's `background: true`); on by default since 2026-10-13. Foreground and background sub-agent runs alike now emit spawned/finished lifecycle frames — foreground runs also stream progress frames — that the Ocean pager renders as a live sub-agent roster. | unset (on) | | `OC_WEBSEARCH_PROVIDER` | Force the web-search provider: `exa` or `parallel`. | chosen per session | | `OC_MCP_META_TOOLS` | `1` swaps the flat MCP tool catalog for the `search_tool`/`use_tool` meta-dispatch once the connected tool count passes 64. Above 128 tools the swap always happens (providers reject a longer `tools` array). See [11 · MCP servers](/oceancode/mcp). | unset (off below 128; forced on at 129+) | | `OC_MCP_WAIT_SECONDS` | Whole seconds a prompt's first model request waits for the MCP servers configured at boot to finish connecting, so their tools are in that request instead of arriving in the next one (a tool list that grows between two steps of a prompt re-writes the provider's cache). `0` never waits; a server still connecting at the bound is not waited for. Sub-agents never wait. See [11 · MCP servers](/oceancode/mcp). | `5` | | `OC_LOG` | Engine log level for the redacting tracing subscriber: `trace`, `debug`, `info`, `warn`, or `error`. Overrides the `obs.log_level` config key; an unrecognized value logs a one-line notice and falls back to `info`. Secret-shaped values are masked before any log write. See [18 · Operations](/oceancode/operations#log-levels-oc_log). | unset → `obs.log_level` (default `info`) | | `OC_TERMINAL` | Set to `1` by the app in every shell and tool it spawns, so tools can detect they run under oceancode. Exported, not read. | set by the app | | `OC_HOOK_EVENT` | Set by the app on every hook command handler to the firing event's name (`PreToolUse`, `Stop`, …). Exported, not read. See [10 · Hooks](/oceancode/hooks). | set per hook invocation | | `OC_PROJECT_DIR` | Set by the app on every hook command handler to the project directory (also exported as `CLAUDE_PROJECT_DIR`, and used as the handler's working directory). Exported, not read. See [10 · Hooks](/oceancode/hooks). | set per hook invocation | | `OC_MODEL_FORMAT` | Legacy tool-result format switch; no longer read — every result renders in the native markdown format. | inert | | `OC_TUNNEL_TOKEN` | Platform build. The OceanTunnel session token, read at the highest priority — above `OCEAN_TUNNEL_TOKEN`, then `ocean.tunnel_token_file`, then the inline `ocean.tunnel_token`. It supplies a token; it does not turn platform mode on. See [15 · Platform mode](/oceancode/platform-mode). | unset | | `OCEAN_TUNNEL_TOKEN` | Deprecated: still read as a legacy fallback for `OC_TUNNEL_TOKEN` (logs one deprecation warning per process); set `OC_TUNNEL_TOKEN` instead. See [15 · Platform mode](/oceancode/platform-mode). | unset | | `OC_SSO_TOKEN` | Platform build. A per-request gateway SSO session token. When set, the route layer sends it as the `X-Ocean-SSO-Token` request header (no `Bearer` prefix) on the configured **https** gateway host only, leaving the wallet `Authorization: Bearer` (your `ozk_` key) intact; read at runtime, never persisted. NOTE: the inference gateway does not yet read `X-Ocean-SSO-Token`, so it is ignored server-side until that lands. To store one, use `oceancode login --sso-token`. See [15 · Platform mode](/oceancode/platform-mode). | unset | | `OC_JEV` | `off`, `shadow` or `act`: overrides every Jev decision point's mode (`off` turns the layer off whatever the config says). It never supplies a key — without one the layer stays off. | unset → the config | | `OC_JEV_ENDPOINT` | Overrides `jev.endpoint` (the Jev `POST` URL). The only way to use plain `http`, and only to a loopback host (`127.0.0.1`, `localhost`, `::1`). | unset → `jev.endpoint`, else `https://api.typesafe.ai/v1/systemone` | | `OC_TYPESAFE_API_KEY` | The TypeSafe API key when none is stored (`oceancode key typesafe` wins), sent only to `api.typesafe.ai` (never to another host); never in config; stripped from child processes like every `*KEY*` variable. The direct API without a key keeps the layer off. | unset | | `TYPESAFE_API_KEY` | The same key under the name TypeSafe's own SDKs read; `OC_TYPESAFE_API_KEY` wins over it. | unset | ### Test and bench knobs These steer test, benchmark, and profiling runs; they are not part of normal operation. | Variable | Meaning | Default | |---|---|---| | `OC_DETERMINISTIC` | `1` pins ids, clocks, and other non-determinism for reproducible runs and replay. | unset | | `OC_METRICS_ADDR` | `host:port` to serve runtime gauges (uptime, worker/task counts) for profiling. | unset (nothing listens) | | `OC_TEST_HOME` | Overrides the home directory used for path resolution, for tests. | the real home directory | | `OC_CACHE_HINTS_MUTATE` | Toggles prompt-cache-hint mutation, for cache experiments. | unset | | `OC_CACHE_KEEPALIVE_MINUTES` | Minutes between prompt-cache keep-alive touches. While a step waits (a sub-agent, a long `bash`) or the session idles, the engine re-sends the last request with `max_tokens: 1` at this interval so the provider's cached prefix is read again and its one-hour clock restarts — a cache read instead of the full re-write a cold return pays (about 1:80 on Fable 5.1: 0.025× read against a 2× one-hour write, ~$0.06 against ~$5 on a 250k prefix). `0` or `off` disables it. Each touch is a `session.cache.touched` event and its cost joins the session's spend. | `50` | | `OC_CACHE_KEEPALIVE_MAX` | Touches per request before the keep-alive gives up (≈ 40 h at the default interval; a model's break-even against one 2× re-write is 2× ÷ its cache-read price: 80 touches on Fable 5.1, 20 on a 0.1× model). | `48` | | `OC_CONFORMANCE_TARGET_URL` | Target server URL for the conformance harness. | unset | | `OC_TEST_TIMEOUT_SCALE` | Multiplies harness timeouts (`ocean-testkit`, dev-only); floored at `1.0`, so it can only lengthen, never shorten, a timeout. | unset (`1.0`) | | `OC_LIVE_CONFIRM` | `1` arms the money-gated live benchmark drivers (`scripts/bench/task11-stratified.sh`, `swebench-parity.sh`, `marathon-month-gate.sh`, `run-official-board.sh`) to spend against a real model; unset, each refuses to spend and only `--self-test`s. Never set by `check.sh`. | unset (never spends) | | `OC_SQLITE_CACHE_KB` | Per-connection SQLite page-cache size in KiB (`PRAGMA cache_size = -<kb>`), applied to every writer and read-pool connection. A soak/measurement knob for isolating engine memory from the box's page cache; leave unset in normal use. | unset → SQLite default (≈64 MB) | | `OC_READ_POOL_SIZE` | Number of connections in the session store's read pool. A soak/measurement knob to bound per-connection memory when profiling; leave unset in normal use. | unset → `available_parallelism()` | | `OC_PAGER_DIALECT_COVERAGE_OK` | Not read from the environment: the sentinel line the shared ACP dialect's coverage test prints (`ocean-acp` `pager_dialect_coverage_ok` in `extensions/dialect.rs`: `methods=<n> implemented=<n> unmapped=0` — every `Implemented` ext method has a dispatch arm). Listed because the name has the `OC_` shape this table is checked against. | — | | `OC_JEV_FIXTURE_DIR` | Directory the Jev policy-table tests read their recorded fixtures from, instead of each crate's `tests/fixtures/jev` (read by `ocean_jev::testkit::load_fixture`; set by `scripts/gates/check-jev.mjs --self-test` to a planted copy). Test-only; the binary never reads it. | unset (the crate's own fixtures) | | `OC_LLM_CACHE_DIAG` | Anthropic cache diagnostics are on by default: every request opts in, first with a null previous ID, then with that session’s previous response ID. `0` disables them. The API reports structural divergence, pending or unavailable comparisons; token usage determines whether the cache hit. IDs reset on restart or session deletion. | `1` | | `OC_LLM_DUMP` | Directory to write every provider-native request body into as `<seq>.json` (a process-wide counter), so two consecutive steps can be diffed byte for byte in a prompt-cache audit (read by `ocean_llm::route::client`). Diagnostic; bodies contain the whole context — use a private directory and delete it afterwards. | unset (off) | | `OC_TOOL_REPLAY` | Path of a recording (`{"<tool call id>": {"ok": bool, "text": string}}`); the tool registry then answers every call from it by the model's call id instead of running the tool (an unrecorded id is an error result), so a recorded `bash` or `edit` never runs again. Compiled in only with `ocean-agent`'s `test-util` feature (`cargo build --release -p ocean-cli --features test-util`, which `scripts/probes/real-replay-probe.sh` does into `target/replay-bin`) — the shipping build never reads it. An unreadable or malformed recording stops the process. | unset | ## Claude Code settings mirror On the subscription route (`claude-code/…` models, run through the `claude` CLI) OceanCode follows Claude Code's own settings, so both tools start the same way. It reads three files, lowest precedence first, and merges them per key (`modelSettings` per model): | Tier | Path | |---|---| | user | `~/.claude/settings.json` | | project | `<project>/.claude/settings.json` | | local | `<project>/.claude/settings.local.json` | | Key | What OceanCode does with it | |---|---| | `model` | With no `model` in `oceancode.json` and `claude` on this machine, the catalog default and every new session are pinned to `claude-code/<model>` — the id verbatim, `[1m]` suffix included (`--model claude-fable-5-1[1m]`). An explicit `model` in `oceancode.json` wins. | | `effortLevel`, `modelSettings.<model>.effortLevel` | The effective effort (`modelSettings` over `effortLevel`, matched with and without the `[1m]` suffix) becomes the pinned variant and reaches the CLI as `--effort <level>` — `low`, `medium`, `high`, `xhigh` or `max`; any other value is shown but not passed. `ultracode` plays no part: since Claude Code 2.1.284 it is its own toggle at any effort level, so the written effort (the fleet router's `max`) is what the row, the pinned variant and the wire carry (2026-09-29) | | `ultracode` | Drives the status bar's `⚡ ultracode on` mark (and the persisted preference). `/ultracode` still toggles it by hand until the next settings change. Claude Code honours the same key (`"ultracode": true` makes every Claude Code session ultracode). | | `env.CLAUDE_CODE_SUBAGENT_MODEL` | The sub-agent model the fleet router pins. Applied on the native route while the subscription slot is live (2026-09-29): shown on the identity row as `sub-agents <name>` and the model a session's children run, as `anthropic/<id>` at that id's `modelSettings` effort — unless `/subagent` overrides it per session (the override holds until the router pushes a new value, then it is cleared). A router push re-seeds running sessions without a restart. | | `env.CLAUDE_CODE_SUBAGENT_MODEL_FORCE` | `"1"` marks the router's sub-agent model as forced. Reported (`GET /claude/settings`) and read alongside the model; not enforced — an explicit `model` on a task call still outranks the router's default. | | `alwaysThinkingEnabled` | Reported (`GET /claude/settings`); not yet acted on. | | `hooks` | See [10 · Hooks](/oceancode/hooks#where-hooks-are-read-from--and-when-they-run). | **Live.** The engine watches both `.claude` directories. Saving any of the three files re-reads the mirror and the hooks within about a quarter second: sessions still on the previously mirrored model move to the new one, except a session whose agent has its own `agents.<id>` row (a model you picked by hand in `/models` is kept), and the UI shows `Claude Code settings reloaded — <model> · effort <level> · ultracode on|off` when a mirrored value changed (the fleet router rewrites the file periodically; an unchanged rewrite is silent). A save of `oceancode.json` is live too: the pager toasts `Settings applied: …` with each value that moved, and an open session still on its agent row's old value follows the row ([19](/oceancode/pager#shifttab-ring-and-permission-modes)). Nothing is mirrored when `claude` is not installed (the settings are still read and reported) or when the files name no `model`. Managed (enterprise) settings are not read. The watcher follows the `.claude` directories that exist when the engine starts — a project `.claude/` created afterwards is read at the next start (or `/hooks` → `r`). ## Reasoning effort per provider Set the effort in `oceancode.json` for every role, on any provider: `effort` (the main session), `subagent.effort` (sub-agents) and `agents.<id>.effort` (the effort a session runs at after Shift+Tab to that agent, clamped to the model's levels; with `agents.<id>.model` also set, a `task` call naming that model runs at it too). Option+Tab (or F3) in the pager steps the current session's effort through the model's levels; the next Shift+Tab loads the agent's default again. A write to the user or the project `oceancode.json` applies without a restart: running sessions keep their model and effort, new sessions and the next sub-agent get the new values. Every model is offered its own reasoning levels, taken from models.dev `reasoning_options`: the model's effort list as listed (e.g. `minimal`…`high` on Gemini 3 Flash, `low`/`high`/`max` on GLM-5.3); `on`/`off` for a model that can only switch thinking on and off; `low`/`medium`/`high` for a model that takes only a thinking budget (1024 / 8192 / 24576 tokens, clamped into the model's range). A toggle model also offers `off`. Models without models.dev data (seed and config entries) fall back to the built-in Claude and GPT-5 tiers. The level you pick is sent as named — never mapped to a different one — and a level the model does not list sends nothing. | Protocol / provider | Request field sent | |---|---| | Anthropic Messages | `thinking: {type: "adaptive"}` + `output_config.effort` (Claude 4.6+); budget-token `thinking` on older Claude | | OpenAI Responses (`openai`) | `reasoning.effort` (+ `reasoning.summary: auto`) | | OpenAI Chat, provider `openai` | `reasoning_effort` | | OpenAI-compatible chat (xAI, Z.AI, DeepSeek, Groq, Cerebras, OpenRouter, …) | `reasoning_effort`; a toggle model also sends `thinking: {type: "enabled"}`, and `off` sends only `thinking: {type: "disabled"}` | | Gemini (Google AI Studio) | `generationConfig.thinkingConfig.thinkingLevel` (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`, Gemini 3+) or `thinkingConfig.thinkingBudget` (Gemini 2.5; `off` = `0` where the model allows it) | | Bedrock, Azure, GitHub Copilot, Cloudflare | nothing yet — no effort menu is shown | ## See also - [01 · Getting started](/oceancode) — where these files live on disk. - [07 · Headless and server](/oceancode/headless-and-server) — `OC_SERVER_USERNAME` / `OC_SERVER_PASSWORD`. # Plan mode and todos Source: https://www.plungeai.com/docs/oceancode/plan-mode-and-todos Two separate things share the word "plan": the **step list** the model keeps as it works, and **plan mode**, an agent that researches and writes a plan file before anything is changed. For the full design and the code paths, see PLAN-MODE.md (`apps/ocean-coding-agent-rust/docs/PLAN-MODE.md` in the repository); this page is the operator's summary. ## The step list (todos) For work with three or more distinct steps the model keeps a todo list — one item per step — and Ocean pager's todos pane shows it live. A single edit, one command or a plain question gets no list: the same rule Claude Code, opencode, Kilo Code and Grok Build use. Each row ticks through four states: | Glyph | Meaning | |---|---| | `[ ]` | pending | | `[»]` | in progress | | `[✓]` | completed | | `[✕]` | cancelled | The model writes the list as a fenced ` ```todo ` block in its reply (or through its todo tool); the newest complete block replaces the whole list, so the pane always reflects the latest one. In the pager, `ctrl+t` toggles the todos pane (see [19 · Ocean pager](/oceancode/pager)). If the model skips the list and then does real multi-step work anyway (twelve or more tool calls with no plan), the next step carries a reminder to write it; a plain answer is never reminded. A settled turn that still has open items is worked off (see [08 · Bounding a long run](/oceancode/bounding-a-long-run)). `OC_PLAN=off` turns the directive, the reminder and the work-off off for a run; the todos pane and `/plan` keep working. ### The always-plan floor If the model has not planned yet, `oceancode` shows one synthesized row — your prompt text — so the list is never empty. The moment the model writes a real plan, that floor steps aside and does not come back for the session. ### Editing the plan yourself — `/plan` You can edit the list while a step runs; the model merges your changes with its own on the next step. | Command | Effect | |---|---| | `/plan add <text>` | Add a pending row. | | `/plan done <n>` | Mark row `n` completed. | | `/plan cancel <n>` | Mark row `n` cancelled. | | `/plan edit <n> <text>` | Rewrite row `n`. | | `/plan remove <n>` | Delete row `n`. | A row you add is open work: the run keeps going until it is done or a bound trips (see [08 · Bounding a long run](/oceancode/bounding-a-long-run)). If you add a to-do while the agent has already finished, it counts as progress — the run picks the work back up for one more turn instead of stopping. ## Plan mode Plan mode is a distinct **agent**, not a key. In it the model researches and writes a plan, and may change nothing else: it may read and search, run read-only commands and send out explore agents, and its one writable place is the plan folder, `.oceancode/plans/` (`.md` and `.html` files only). - **Enter it** by selecting the `plan` agent — in Ocean pager, a bare `/plan` or `/plan <description>`, or Shift+Tab to `Plan` (the row-edit verbs above are the only `/plan` forms that do not switch mode). - **The plan file.** Every plan-mode session writes one file, `.oceancode/plans/<date>-<topic>-<tag>.md`, where `<tag>` comes from the session id. It holds: - the goal and the research findings, with `file:line` references; - the chosen approach; - a Mermaid workflow diagram; - the to-dos as one ` ```todo ` block, with sub-steps indented; - a verification check per to-do, and any open questions. The model updates the same file as the plan changes. It writes an `.html` page beside it only when you ask for one. The folder lives in your project, so the plan is there to read, edit or commit. - **Review and leave it.** When the plan is ready the model calls `plan_exit`. That refuses until the plan file exists with its to-do block, then asks you to review it. In Ocean pager the plan opens in the plan viewer, where you can comment on lines and then: - **approve** (`a`): the plan's to-dos become the session's to-do list, the file is marked `status: approved`, and the session switches to the `build` agent. Build first checks the plan's findings against the code, splits large to-dos into sub-to-dos, and then implements them one by one, keeping the file's status current. The run keeps going until every to-do is done (see [08 · Bounding a long run](/oceancode/bounding-a-long-run)); - **send it back** (`s`, type your notes, Enter): the model stays in plan mode, revises the file with your notes and line comments, and asks again; - **abandon** it (`q`): plan mode ends and nothing is built. Other editors (Xcode) show the same choice as a question: `Build`, `Keep planning` or `Abandon`. You can type feedback there too. The step list keeps working throughout: plan mode changes *what the model may do*, not how progress is tracked. ## See also - [06 · Session ledger and export](/oceancode/session-ledger-and-export) — where the plan and turns are recorded. # Session ledger and export Source: https://www.plungeai.com/docs/oceancode/session-ledger-and-export A long session would grow to a huge, ever-slower context. `oceancode` avoids that with the **session ledger**: the model sees a small, fixed window of recent turns, and everything older is kept verbatim in Markdown files beside the session — never summarized away. The full concept is in SESSION-LEDGER-CONCEPT.md (`apps/ocean-coding-agent-rust/docs/SESSION-LEDGER-CONCEPT.md` in the repository); this page is the operator's view. ## What the ledger is - **A window of the raw conversation.** The model always sees the newest part of the real conversation, up to a bounded size — by default **40% of the model's context** (400k on a 1M model, 80k on a 200k model). The window is measured in the provider's real tokens after every step, and the oldest content leaves it one **block** at a time — inside a long turn too (see **The tideline** below). `tokens: 0` instead tracks the model's own ceiling — its context window minus the output allowance; 120k only when the route reports no limit. A *turn* is your message plus the model's full answer, tool outputs included. - **Per-turn transcripts on disk.** Every step is written verbatim to its turn's transcript file as it ends, and the index files are updated when the turn completes. As the window grows the oldest blocks leave it — they are already on disk, so nothing is lost and no model call is made. The step's write happens *behind* the step: the next step does not wait for it. `recall` and the session meter **settle** the ledger first (they wait for every queued write), so they always see the step that just ended. A plain `read` of a transcript file does not wait: each append replaces the file atomically, so a read is never torn — at most it is one step behind. A pruned tool output is only ever pointed at a transcript line that is already written. - **`SESSION.md`, one living page.** Goal, decisions, environment facts, errors and their fixes, a code map, open questions, and the next step — each line stamped with the turn it came from. Facts the database already holds (files touched, commands and exit codes, tasks, the log) are written by the engine. The page is *edited*, never rewritten, and by one writer; reversed decisions keep the old text struck through rather than deleted. The ledger ships **enabled** (`ledger.enabled: true`), so every session is laddered from its first turn with no setup; its per-turn transcripts are kept as long as the session is: `retention.days` (default 7 days idle) removes a session and its ledger directory together — see [18 · Operations](/oceancode/operations#retention-and-vacuum). See [02 · Configuration](/oceancode/configuration) to change either. **OceanCode never summarizes a session** — the legacy compaction (summarization) path runs only when `ledger.enabled` is `false`. **How an open session is stored.** An open session lives in memory: every step reads its history from there, and its rows in `oceancode.db` are written *behind* the step, in batches, by the one database writer — a step never waits on the disk, however long the session grows. Anything that reads the database rather than the open session (an export, opening a session that already left memory, the retention sweep) first **settles** the writer: it waits for every queued write, so it sees the step that just ended. A session idle for 15 minutes leaves memory and is reloaded from the database on its next use. **What a crash loses:** only the writes still queued when the process died — normally the last few milliseconds (`oceancode_writer_lag_ms` on the metrics endpoint, see [18 · Operations](/oceancode/operations#metrics-oc_metrics_addr)). What is on disk is always an in-order prefix of the session — no gap in its event log, no row ahead of the event it came from — so a resume continues from the last committed step. Find a session's ledger directory with: ```bash oceancode ledger path <session-id> ``` ## The tideline — `/tideline` You set **one number**: the share of the model's context where the tideline sits (`ledger.tideline.percent`, default `40`) — the **line**. After every step the ledger reads the window's size in the provider's real tokens (the **gauge**: input + cache read + cache write of the step that just ended, plus what that step added — its output and tool results; with `compress` on, plus the compressor's saved estimate, removed bytes ÷ 4, so the gauge still measures the uncompressed window). Steps are grouped into **blocks**: a block closes once the gauge has grown by `ledger.tideline.block` since it opened — a token count, or a share of the line written `"35%"` (unset = 70% of the line, so 70 000 at a 100 000-token line). While the gauge is at or over the line, the oldest closed block leaves the window, so the window drops to about line − block and grows again. The block still being filled is never cut, so the window overshoots the line by at most one block. A single turn larger than the line is cut block by block while it runs; your message in the oldest block leaves with it. After a cut the window opens with one line that points at the ledger: `[Earlier content of this session is past the tideline: every row up to seq <n> left this window. The verbatim record is <ledger>/transcript/ (one file per turn) and SESSION.md carries the merged state. …]`. A turn whose every row is past the tideline is merged into `SESSION.md`; a turn cut only in part is not. If the provider still refuses a request as too long, every closed block is cut before the one retry. Between cuts every step only appends, so the provider's prompt cache hits; each cut re-writes the retained window once. The block size sets how often: a 50k block cuts about 20 times per 1M new tokens past the line, a 20k block 50 times. When the model's context is unknown the fixed `tokens` (default `300000`) is the line. A line above what the model can hold (its context minus the output allowance, at most 32k) is clamped to that, with one note in the log. The status bar's context row measures against the model's own context (`…/1.0M` on `claude-fable-5-1[1m]`); the `┃` on its gauge sits at the tideline — `tideline @40% (400k): 238k to go`, or `tideline @300k: 138k to go` under a fixed limit — and it reads `tideline off` when the ledger is disabled. Config examples (`ledger.shelve` and `ledger.window` are still read as aliases): ```json { "ledger": { "tideline": { "percent": 50 } } } { "ledger": { "tideline": { "tokens": 500000, "block": 20000 } } } ``` `tokens: 0` means the model's own ceiling. The older keys — the floor (`low`), `reserve`, the `profile` presets (`wide` · `narrow` · `strict` · `auto`) and `ledger.hard_tokens` — were removed with the band geometry: an old config still loads, they are ignored, and the load logs one warning naming them. Change the tideline for a session at runtime with `/tideline`: | Command | Effect | |---|---| | `/tideline` | Show the current tideline state. | | `/tideline now` | Move the tide now (a cut). | | `/tideline 50%` | Set the limit as a percent of the model's context. | | `/tideline 500k` | Set a fixed limit. | | `/tideline default` | Restore the configured limit. | Minimum fixed limit 20k; `k` and `m` suffixes are accepted. The floor and the presets were removed; a second number or a preset word is refused with the usage line. A change applies from your next message on, which the confirmation says. Headless, the same set is `--tideline "<percent>% | <limit> | default"`. A sub-agent (child) session inherits its parent's `/tideline` override — the percentage, or the limit. The meter's denominator is the model's context window under `auto` and the effective window otherwise. ## The session meter Ocean pager has no cockpit panel; its status bar and `/context` show the session meter: - the **context meter** — what the current window costs against the session's window size; - **token counts** — input and output for the session; - **spend** — `$spent / $cap` on a metered route. On the Claude Pro/Max subscription route the bill is a list-price *estimate*, shown as `≈$spent list · subscription`; a `limits` row appears when a step or time bound is set. On the Claude Code route the meter's **context** number is Claude Code's own real context — its system prompt, `CLAUDE.md` files and MCP schemas, ~100k+ before any turn. When it reaches the limit OceanCode drops the `--resume` id and cold-starts Claude Code with the ledger page plus the retained turns' text (tool outputs are not re-sent — the page's links carry them), so the number restarts near that baseline and climbs again. ## Recovering a step — `/undo` and `/redo` `/undo` (`/rewind`) reverts the conversation to a previous message and puts back every file that turn and the later ones changed through the file tools (`edit`, `write`, `apply_patch`, sub-agents' included); `/redo` re-applies both. It steps a bookmark through the session, so you can back out a bad turn without losing the history. As in Claude Code, changes a `bash` command made are not tracked. ## Exporting a session Write a whole session out as one file: ```bash oceancode export <session-id> --md [--out <path>|-] # one Markdown document oceancode export <session-id> --atif [--out <path>|-] # one ATIF-v1.8 trajectory (JSON) ``` Pass exactly one of `--md` or `--atif`. `--out -` (the default) writes to stdout; `--out <path>` writes a file. The Markdown export carries frontmatter, every turn, and a `## Summary` table, and is built from the ledger's own transcript files when they exist. ## Rebuilding the ledger If a ledger directory is lost or a session predates the ledger, rebuild it from the database: ```bash oceancode ledger rebuild <session-id> # mechanical: transcripts, indexes, page oceancode ledger rebuild <session-id> --merge # also fold the merge backlog (model calls; asks y/N) oceancode ledger rebuild <session-id> --merge --yes # skip the confirmation ``` The plain rebuild makes no model calls; `--merge` does, so it is never implied. ## See also - [02 · Configuration](/oceancode/configuration) — the `ledger` and `database` config sections. - [08 · Bounding a long run](/oceancode/bounding-a-long-run) — spend and clock bounds. # Headless and server Source: https://www.plungeai.com/docs/oceancode/headless-and-server `oceancode` runs without a UI two ways: a one-shot `run` for scripts and CI, and a long-lived `serve` that exposes the engine's HTTP surface to clients. The terminal UI is Ocean pager, which embeds the engine in its own process (see [19 · Ocean pager](/oceancode/pager)); `serve` is the headless surface. ## One-shot — `oceancode run` Run a single non-interactive turn and exit. ```bash oceancode run "explain what this repo does" oceancode run --format json "fix the failing test" oceancode run --continue "and now add a test for it" ``` Flags go before the prompt (a flag after it is sent to the model as text, with a warning). When stdin is not a terminal, `run` reads it to EOF and appends it to the prompt — the piped body is the instruction — so a script that passes the prompt as an argument must close stdin (`oceancode run "hi" </dev/null`) or the run waits forever (observed 2026-10-05: four minutes of nothing with stdin left open). Useful flags: | Flag | Meaning | |---|---| | `--output-format <fmt>` | `stream-json` (newline-delimited events, then the final `result` envelope), `json` (a single buffered `result` object), or `text` (human, default). Supersedes `--format`. | | `--format json` | Deprecated alias of `--output-format stream-json` — output unchanged. Emits newline-delimited events, then one final `result` envelope (`ok`, `steps`, `durationMs`, `messages`, `tokens`, `cost`) — written on the failure path too. | | `-r <title>` / `--resume-title <title>` | Resume the most recent session whose title matches `<title>`. | | `--tools <a,b>` | Restrict the advertised tool set to these ids (an unknown id is an error). | | `--disallowed-tools <a,b>` | Remove these ids from the advertised set. | | `--allow <Tool(pat),…>` | Append `allow` permission rules for the run. | | `--deny <Tool(pat),…>` | Append `deny` permission rules (last-match-wins; a `--deny` beats an overlapping `--allow`). | | `--append-system-prompt <text>` | Append text to the run's system context. | | `--json-schema <path\|json>` | Structured output: validate the model's final JSON against this JSON Schema (a file path or inline JSON). Threads the schema into the prompt, re-prompts with the validation errors up to a bounded retry cap. On success the validated object is the result — printed bare in text framing, or as `result.structured` on the `--format json` result envelope (exit 0). An unsatisfiable or uncompilable schema exits **6** with the typed error; a run cut short keeps its typed exit code (budget 3 / time 4 / no-progress 5 / permission 2 / interrupt 130). | | `--continue` / `-c` | Continue the last session. | | `--session <id>` / `-s` | Continue a specific session. | | `--fork` | Fork the session before continuing (needs `--continue` or `--session`). | | `--agent <name>` | Start in a specific agent. | | `--model <provider/model>` / `-m` | Use a specific model. | | `--directory <dir>` | Working directory (defaults to the current one). | | `--db <path>` | Session database file. | | `--atif <path>` | Write the session's ATIF-v1.8 trajectory (JSON) to `<path>` at the end of the run — the same document `oceancode export --atif` produces. Empty/unset = off. See [06 · Session ledger and export](/oceancode/session-ledger-and-export#exporting-a-session). | | `--subagent <provider/model>` | `/subagent` for a headless run: the sub-agent model this session's children run (e.g. `openai/gpt-5-mini`). Written onto the session (survives `--continue`). A value that is not `provider/model` is refused with the same words `/subagent` uses. Empty/unset leaves whatever the session already carries. | Bound flags (`--max-usd`, `--max-minutes`, `--max-steps`, `--tideline`, `--subagent`) must come **before** the prompt — the parser stops at the first non-flag word, and a bound placed after the prompt is ignored with a warning. `--tideline <pct>%` sets the tideline as a share of the model's context (e.g. `--tideline 50%`); `--tideline <limit>` or `default` also work. The floor and the presets were removed: a second number or a preset word is refused with the usage line. The old `--window` name is refused with the hint `use --tideline`; `--shelve` still works as an alias. See [08 · Bounding a long run](/oceancode/bounding-a-long-run). The engine also serves `GET /session/{id}/subagent` → `{"model": <ModelRef> | null}` — the session's effective sub-agent model (the `/subagent` override, else the fleet router's `CLAUDE_CODE_SUBAGENT_MODEL` while the subscription slot is live, else the `subagent.model` config, else the session's own model). A client reads it for the identity row's `sub-agents <name>` segment; an engine without the route answers as "nothing to show". ## Host-run workflows — `oceancode workflow` Launch and control a host-run Rhai workflow without a UI. `workflow run` boots the engine the same way `oceancode run` does, launches the workflow, streams each `workflow_updated` body as one JSON line on stdout, and exits by the run's outcome. ```bash oceancode workflow run pipeline --args '{"steps":[{"prompt":"say A"},{"prompt":"say B"}]}' oceancode workflow run ./research.rhai --budget 64 --model openai/gpt-5-mini oceancode workflow run pipeline --validate-only --args '{"steps":[{"prompt":"x"}]}' oceancode workflow runs --all oceancode workflow stop wf-… ``` The workflow **name (or a path to a `.rhai` file) comes first**, before any flags. A registered name (a built-in such as `pipeline`, or a discovered `.oceancode/workflows/<name>.rhai`) is resolved by the launcher; a path you type is read and run as an inline script. | Flag (of `workflow run`) | Meaning | |---|---| | `--args <json>` | A JSON object bound to the script's `args` global (e.g. a pipeline's `steps`). Empty = `{}`. | | `--budget <n>` | Absolute cap on cumulative child-agent calls for this run, `1..=1024`. Unset uses the `workflows.agent_budget` config default. | | `--validate-only` | Run a metadata + compile smoke check only — prints a `validated` body and launches no agent (no provider call). | | `--model <provider/model>` / `-m` | Fill this model in for workflow steps that specify none (a pipeline's `args.steps[].model`, else top-level `args.model`). It never overrides a model a step already carries, and there is **no default provider** — a step left unset follows the session policy. | | `--directory <dir>` | Working directory the run and its sub-agents use (defaults to the current one). | | `--db <path>` | Session database file. | | `--verbose` / `-v` | Trace outbound LLM provider calls to stderr. | **Exit codes** (the run's outcome): **0** `complete`; **2** `failed`, `cancelled` or `budget_limited`; **3** a pause (`user_paused`, `back_off_paused`, `no_progress_paused`, `infra_paused`, `blocked`). The last streamed line always carries the terminating body, so a script can read the outcome from stdout as well as from `$?`. `workflow runs [--all]` lists the persisted runs (each `workflow_updated` body, one per line), read from the run directories under the app data dir — scoped to the current project unless `--all` lifts the filter. `workflow pause|resume|stop <run_id>` drive the launcher's control seam; `resume` takes an optional `--budget <n>` to lift a budget-limited run. These control a run **live in the engine this command boots** — a run launched by a separate `oceancode workflow run` has already exited in its own process, so cross-process control is a shared-server concern not wired here. ## Goal mode — `oceancode goal` Prove an objective is done, not merely claim it — headless. `oceancode goal "<objective>"` boots the engine like `oceancode run` does, launches the built-in `goal` workflow (**Plan → Work → Verify**: plan the objective into verifiable deliverables, work in rounds with one resumed worker, and converge only when a cold panel of adversarial skeptics fails to refute completion), streams each `goal_updated` body as one JSON line on stdout, and exits by the run's outcome. It is the headless twin of the pager's `orchestrate: always` goal mode (`19-pager.md`). ```bash oceancode goal "add a --json flag to the status command and prove it works" oceancode goal --budget 64 --model openai/gpt-5-mini "port the parser to the new API" ``` The **objective is the positional argument** — quote it, or let the words after the flags form it. Go-style flag parsing stops at the first positional, so any flag must come **before** the objective. | Flag | Meaning | |---|---| | `--budget <n>` | Absolute cap on cumulative child-agent calls for the whole goal, `1..=1024`. Unset uses the `workflows.agent_budget` config default. | | `--model <provider/model>` / `-m` | Run-level default model: pins every one of the goal's agents (planner, worker, skeptics) whose own step set no model. There is **no default provider** — unset, each agent follows the session policy. | | `--directory <dir>` | Working directory the goal and its sub-agents use (defaults to the current one). | | `--db <path>` | Session database file. | | `--verbose` / `-v` | Trace outbound LLM provider calls to stderr. | **Exit codes** (the run's outcome): **0** `complete`; **2** `failed`, `interrupted` (a stopped run) or `budget_limited`; **3** a pause (`user_paused`, `back_off_paused`, `no_progress_paused`, `infra_paused`, `blocked`) — a goal that stalls on identical gaps two rounds running, or spends its budget, pauses rather than looping forever. The last streamed line always carries the terminating body, so the outcome reads from stdout as well as from `$?`. A finished goal is also listed by `oceancode workflow runs --all` (it shares the workflow run store). ## Long-lived server — `oceancode serve` ```bash oceancode serve # loopback, port 4096 oceancode serve --port 8080 ``` | Flag | Meaning | Default | |---|---|---| | `--port <n>` | TCP port to listen on. | `4096` | | `--host <ip>` | Interface to bind. Loopback by default; a non-loopback host refuses to start without `OC_SERVER_PASSWORD`. | `127.0.0.1` | | `--db <path>` | Session database file. | `oceancode.db` | | `--directory <dir>` | Working directory. | current directory | | `--verbose` / `-v` | Trace outbound LLM provider calls. | off | The default shape (`oceancode`, the pager) passes `--listen <host:port>` through to the engine it embeds; a non-loopback bind refuses to start without a server password (below). The guard is enforced inside the server for both `oceancode --listen` and `oceancode serve --host`. ## Authentication HTTP basic auth is off until you set a password. | Variable | Meaning | Default | |---|---|---| | `OC_SERVER_PASSWORD` | The basic-auth password. Empty means auth is disabled; any value turns it on for every route, SSE included. | unset (off) | | `OC_SERVER_USERNAME` | The basic-auth username. | `oceancode` | A non-loopback bind (`--listen` on the default shape, or serving to a public interface) refuses to start unless `OC_SERVER_PASSWORD` is set. ## Managing capabilities from the CLI Besides `run` and `serve`, the binary answers a set of non-interactive management subcommands that boot no session. Each is read-only or writes a single config file, and exits. | Subcommand | What it does | |---|---| | `inspect [--json]` | Print the provenance of every config key, capability, MCP server and instruction file for the current directory. See [09 · Capabilities and plugins](/oceancode/capabilities). | | `doctor` | Report crash-recovery state: orphaned sessions (registry entries whose process died without a clean exit) and captured panic reports found under the data dir; prints `no orphaned sessions` when clean. Read-only. See [12 · Permissions and sandbox](/oceancode/permissions-and-sandbox#managed-enterprise-config). | | `plugin …` | Manage plugins and marketplaces: `marketplace add\|list\|update\|remove`, `install`, `list`, `enable`/`disable`, `trust [--off]`, `validate`, `update`/`uninstall`, and `import --from claude-code [--trust] [--no-repo-trust] [--dry-run]` (copy Claude Code's plugins, register its marketplaces, seed trust). See [09 · Capabilities and plugins](/oceancode/capabilities#importing-from-claude-code). | | `mcp …` | Manage MCP servers: `add` (`--command`/`--url`), `list` (every discovered server with `name kind tier trusted enabled source`), `enable`/`disable`, `remove`, `login`/`logout`, `doctor`. See [11 · MCP servers](/oceancode/mcp). | | `skill …` | `skill create <name> [--description <text>]` scaffolds `.oceancode/skills/<name>/SKILL.md`; `skill list [--json]` prints every discovered skill with its tier, trust and enable state. See [09 · Capabilities and plugins](/oceancode/capabilities#skills--browse-and-run). | | `hooks …` | `hooks list [--json]` prints every hook source file the boot loader reads (tier, trust, loaded or held and why) with its handlers and ends with `HOOKS_OK sources=<n> handlers=<m> loaded=<k>`; `hooks add <Event> --command <cmd> [--matcher <re>] [--timeout <s>] [--scope user\|project\|local]` and `hooks remove …` edit the same `settings.json` files Claude Code writes. See [10 · Hooks](/oceancode/hooks#hooks-and-oceancode-hooks-list). | | `jev status [--json] \| probe [--record …] \| labels \| questions --json \| verdicts --in <file>` | The Jev decision layer: each of the nine decision points' mode and endpoint host (`JEV_STATUS_OK points=9`), a one-call live probe (`JEV_PROBE_OK`) and fixture recorder (`probe --record`, `JEV_RECORD_OK`), the export of every labelled decision from the per-session `jev-decisions.jsonl` files into `bench/jev/labels.json` (`JEV_LABELS_OK`), and the seams' question sets and policy verdicts for the bench. Reads the current directory and your environment (no `--dir`/`--home`); only `probe` calls Jev. See [13 · Jev decisions](/oceancode/jev-decisions#the-oceancode-jev-subcommand). | | `trust …` | The persisted per-project trust record (`~/.oceancode/trust.json`): `show` (ends with `TRUST_OK`), `repo on\|off`, `mcp <name> on\|off\|clear`. See [11 · MCP servers](/oceancode/mcp#held-until-trusted). | | `migrate [--db <path>]` | Open and migrate a session database, then exit — the standalone form of the boot-time schema upgrade (`--db` defaults to `oceancode.db`). | | `update [--self-test]` | Self-update to the newest release for the configured channel: verify the release signatures, swap `oceancode` and `ocean-pager` atomically, roll both back on a failed health check. `--self-test` proves the pair swap+rollback offline and prints `UPDATE_OK from=<old> to=<new> rollback_tested=1` then `UPDATE_SELF_TEST_OK binaries=2`. Channel/opt-out via `update.*`, `OC_UPDATE_CHANNEL`, `OC_NO_UPDATE_CHECK`. See [17 · Distribution and updates](/oceancode/distribution). | | `verify-release <artifact> [--pubkey <base64>] [--sha256sums <path>]` | Verify a release artifact's minisign `.minisig` signature (and, with `--sha256sums`, its `SHA256SUMS` entry) in-process — no external tool. Prints `VERIFY_RELEASE_OK file=<name>`. The portable trust check on macOS, Linux and Windows. See [17](/oceancode/distribution#verifying-a-download). | | `uninstall [--purge] [--yes] [--dry-run]` | Remove the binary the installer placed; `--purge` also removes the app's config and data dirs. Requires a typed `oceancode` confirmation unless `--yes`; `--dry-run` reports the plan and removes nothing. Prints `UNINSTALL_OK removed=<n>`. See [17](/oceancode/distribution#uninstalling). | | `login [--provider claude\|copilot\|codex] [--no-browser]` | Log in and store the credential in `auth.json`. `claude` (default) runs the Claude Pro/Max subscription OAuth; `copilot` and `codex` run the OAuth 2.0 device-code flow. See [01 · Getting started](/oceancode). | | `logout [provider]` | Clear the stored credential for a provider (default `anthropic`) from `auth.json`; the counterpart to `oceancode login` / `oceancode auth`. See [01 · Getting started](/oceancode). | | `claude` | Preflight the claude-code provider: report whether the Claude Code CLI is installed and logged in. Advisory only — it never blocks a boot. | Every management subcommand takes `--dir <d>` (the project; default: the current directory) and `--home <h>` (the home the user tier and `~/.oceancode` are read from; default: your home) so a script — or a test — can point it at another tree. ## Permission mode A client reads and switches the session's permission mode over HTTP — the boot default is the `permission_mode` config key, and these two routes change it live (the pager switches it from its own prompt): | Route | Meaning | |---|---| | `GET /api/session/{id}/permission/mode` | Returns `{ mode, bypass_locked }` for the session. | | `POST /api/session/{id}/permission/mode` | Set the mode; body `{ "mode": "acceptEdits" }`. An unknown mode is `400`; `bypassPermissions` while a managed requirements file has locked it is `409`. | The four modes are `default`, `acceptEdits`, `dontAsk`, `bypassPermissions` — see [12 · Permissions and sandbox](/oceancode/permissions-and-sandbox#permission-modes). ## Managed (enterprise) configuration For a shared or locked-down deployment, an administrator layers a managed config and a signed requirements file over every project: - `OC_MANAGED_CONFIG` — path to the managed config file (default: the OS admin path). It is loaded last, so it overrides the project and global files. - `OC_REQUIREMENTS_PUBKEY` — the trusted ed25519 public key (64 hex chars) that a `requirements.json` signature is checked against; an unsigned or tampered file is refused. With no key set, managed requirements are not enforced. Both are described in [02 · Configuration](/oceancode/configuration); the requirements file can disable `bypassPermissions`, allow-list MCP servers, and pin config keys. See [12 · Permissions and sandbox](/oceancode/permissions-and-sandbox#managed-enterprise-config). ## See also - [01 · Getting started](/oceancode) — the three boot shapes. - [02 · Configuration](/oceancode/configuration) — the server variables in one table. # Bounding a long run Source: https://www.plungeai.com/docs/oceancode/bounding-a-long-run A run is bounded by the **money and time you set**, not by an arbitrary step count. Set nothing and a run keeps going until the work is done (or the no-progress guard stops it, below). ## The bounds | Bound | Environment variable | `oceancode run` flag | Slash command (this session) | |---|---|---|---| | Spend cap (USD) | `OC_MAX_USD` | `--max-usd 25` | `/budget 25` | | No spend cap | — | `--max-usd off` | `/budget off` | | Subscription (no cap; spend is a list-price estimate) | — | `--max-usd subscription` | `/budget subscription` | | Wall clock | `OC_MAX_MINUTES` | `--max-minutes 8h` (also `45`, `90m`, `2d`, `off`) | `/limits time 8h` | | Steps per turn (root), 1–1000 | `OC_MAX_STEPS` | `--max-steps 100` (`default` clears) | `/limits steps 100` | | Steps per sub-agent turn | `OC_MAX_SUBAGENT_STEPS` | — | — | | Tideline (% of context) | `ledger.tideline.percent` (config) | `--tideline 50%` | `/tideline 50%` | | Tideline (fixed tokens) | `ledger.tideline.tokens` (config) | `--tideline 60k` | `/tideline 60k` | | Tideline block (what one cut removes) | `ledger.tideline.block` (config: a token count, or a share of the line as `"35%"`; unset = 70 % of the line) | — | — | **Precedence:** slash command / flag > environment variable > built-in default. Flag and slash values are stored on the session, so they survive `--continue`. The tideline's old names still work as aliases: `--shelve`, `/shelve` and `ledger.shelve`. ## The step cap is a boundary, not a stop `OC_MAX_STEPS` (default 300) caps the steps in one root turn, but a root turn that reaches it **auto-continues** into a fresh turn — no budget or clock needs to be set. The ledger cuts by blocks after every step, so even one long turn stays under the tideline (see [06 · Session ledger and export](/oceancode/session-ledger-and-export#the-tideline--tideline)). `0` is rejected: bound a long run with a budget or a clock instead. Sub-agents are different: they simply **stop** at `OC_MAX_SUBAGENT_STEPS` (default 300), so a raised root cap does not deepen them. ## Output tokens `OC_MAX_OUTPUT_TOKENS` overrides the per-request output-token cap in either direction, clamped to the model's own published maximum. It bounds how much a single model response may emit, not the run as a whole; leave it unset to use the model's full maximum. ## What happens at a bound Exceeding a bound is a **clean, non-error stop**, distinct from a crash, and the ledger records it as a step-limited turn. The internal stop reasons are `BudgetExceeded` and `TimeExceeded`. Over the Ocean tunnel the response carries a typed `stopped_by`: | Bound reached | `stopped_by` | |---|---| | `OC_MAX_MINUTES` / `/limits time` | `time-exceeded` | | `OC_MAX_USD` / `/budget` | `budget-exceeded` | | step cap | `step-limit` | ## The plan drives the run The model keeps its plan as a todo list (see [05 · Plan mode and todos](/oceancode/plan-mode-and-todos)). A root turn that settles while any todo is still open does **not** end the run — the engine works off the next open todos and drains again. A run ends only when: - the work settles with no open todo; or - a spend or clock bound trips; or - you interrupt it; or - the **no-progress guard** fires — two consecutive continued turns that leave the open and completed counts unchanged stop the run cleanly with a "Run stopped" toast. So a model that never plans and never settles runs a few turns and stops; one that plans and makes progress runs until its list is done or a bound trips. While a background sub-agent of the session is still working, a settled turn is not worked off: the sub-agent is what moves the plan, and its result re-drives the session when it lands. The work-off resumes in that re-driven turn. Such a turn ends as **waiting**, not finished, and says on what. The pager's end-of-turn line reads `Worked for 6m38s · waiting for sub-agent "Build the mock" · 2 plan rows open · continues when it lands`; an ACP client gets `_meta.oceanStopReason = "waiting"` and `_meta.waitingNote` on the prompt response, and the session's log holds a durable `session.waiting` row. When the sub-agent lands, the engine starts the next turn itself (a durable `session.resumed` row) and the pager draws it as it runs; the sub-agent's result row tells the model how long the run paused, so it can say so when asked. That turn shows as running from its first instant: an ACP client gets `ocean/queue/changed` naming it as running before its first frame, and the pager's status row reads what a turn you started would (`Waiting for response…` during a silent think, `Thinking…`, the running tool, the sub-agent it waits on). Every turn the engine starts this way gets its `session.resumed` row, including the follow-up that answers a result which landed while a turn was running. A message you send while a turn is ending as waiting is answered as its own turn and streamed under its own prompt id. The restart is guaranteed: - a sub-agent that **fails or crashes** still reports, as an error result, and the run goes on; - a sub-agent **you stop** (`ocean/subagent/cancel`, or `esc` `esc` on the session) does not restart the run; if plan rows are left open, the session records why (`reason: user_stopped_child`) and the next message you send continues it; - if the **engine exits** while a sub-agent works (a quit, a crash), the next time the session is opened its lost sub-agents are reported as errors ("the engine restarted while it worked") and the run goes on. A session whose engine is still running elsewhere is left to it. `oceancode run` waits for its background sub-agents and the turns their results start before it exits. ## The Jev supervisor (opt-in) With the Jev decision layer on (see [13 · Jev decisions](/oceancode/jev-decisions)), a supervisor reads a root turn's clean finish and, before a `bash` call whose exact command already ran twice among the prompt's last six, asks whether it would bring anything new (the no-progress check). With `jev.supervisor = "act"` it may keep a finish from ending the turn — a synthesized continuation, at most `jev.supervisor_max_continuations` per human prompt (default 1), only when the goal is clearly not complete and progress is real. Two consecutive no-progress answers send one steer message; four stop the run — under headless `--auto` it ends with stop reason `no_progress` (exit 5), elsewhere you are asked *Continue* or *Stop*. With `jev.check = "act"`, headless `--auto` also ends a run right after a check verified a pass. It never moves a bound: the spend, clock and step bounds above, the exact-repeat doom-loop trip, the hook cap and the no-progress guard decide exactly as they do without it. In `shadow`, the default once the layer is enabled, it only records what it would have done. ## Stopping a run stops all of it `esc` `esc` (or the palette's abort) stops the session's own drain at its next step boundary, and every sub-agent stops with it — foreground ones immediately, background ones cancelled by the parent. A late background result is recorded as cancelled and never restarts the run. ## Background jobs and long waits have no clock of their own A job the model starts — `bash` with `background: true`, a `monitor`, a `loop`, a `scheduler` — runs until it ends or the model calls `job_kill`. It dies with the run only when the run has a wall clock (`OC_MAX_MINUTES` / `/limits time`); without one a build, a watch or a server runs for hours or days, and `job_output` reads it back whenever the model asks. (Until 2026-10-05 every job was reaped ten minutes after launch while the run had no clock at all.) A job that ends on its own — done or failed, not killed — reports back. Its result (state, command, the last 2,000 bytes of output) is posted to the session that started it as a `<job id=… state=…>` message and a new turn starts, the way a background `task` reports; a turn already running gets exactly one follow-up turn. So the model does not have to sleep and poll to learn a job finished, and a turn that stopped polling still hears about it. A job a sub-agent starts wakes no one. Polling is not a loop either. The doom-loop trip ends a turn only after three consecutive fast calls with the same input **and** the same result. A repeat whose output moved (a row count, a timestamp, a log line) or a call that ran 30 seconds or more (a `sleep 300; check`) resets the count to zero. When the trip does fire, the turn ends with stop reason `doom_loop` and the final text `Stopped by the doom-loop guard: …` stays in the transcript, not only in a passing toast. (Until 2026-10-24 three identical `sleep 298; wc -l results.jsonl` polls of a live benchmark tripped it and the turn went quiet with no visible reason.) A long wait does not lose the prompt cache either. Every request's cache markers carry the provider's one-hour TTL, and while a step waits — on a sub-agent, on a long `bash` — or the session idles, the engine re-reads the last request every `OC_CACHE_KEEPALIVE_MINUTES` (default 50) so that clock restarts: a cache read of the prefix (`session.cache.touched`, cents) instead of re-writing it when the wait ends. It stops after `OC_CACHE_KEEPALIVE_MAX` touches (default 48, ≈ 40 h; past a model's break-even — 80 touches on Fable 5.1, 20 on a 0.1× model — a re-write is the cheaper bet). A touch that fails is sent once more a few minutes later before the keep-alive gives up, and pressing Esc on a running turn does not stop it — the next prompt reads the same prefix. See [02 · Configuration](/oceancode/configuration). ## See also - [02 · Configuration](/oceancode/configuration) — the `OC_MAX_*` variables in one table. - [07 · Headless and server](/oceancode/headless-and-server) — the `--max-*` flags on `oceancode run`. # Capabilities and plugins Source: https://www.plungeai.com/docs/oceancode/capabilities Skills, sub-agents, personas, commands and MCP servers are all **capabilities**, and `oceancode` finds them the same way: one discovery walk over five tiers, across four ecosystems, with a single rule for names that collide and a separate rule for what a capability is allowed to do. ## The five tiers A capability is discovered from one of five tiers. The walk runs from the narrowest scope out to the widest, so a name defined closer to the code wins. | Tier | Where it comes from | Example root | |---|---|---| | Local | The project's own `.oceancode/` — your machine's private copy, git-ignored | `.oceancode/skills`, `.oceancode/agents` | | Repo | The project you are in, checked into the repo | `.claude/`, `.opencode/`, `.grok/` | | User | Your home directory, read across every ecosystem | `~/.oceancode/…`, `~/.claude/…`, `~/.grok/…` | | Bundled | Shipped inside the binary | (built in) | | Plugin | An installed plugin's own tree | `<plugin>/skills/`, `<plugin>/agents/` | Plugin-tier roots are enumerated from disk — one root per installed plugin directory — so a plugin contributes its skills, agents and hooks only while it is installed. For plugins Claude Code installed, the roots are **exact**: only the version directories its `~/.claude/plugins/installed_plugins.json` records for the user scope, or for this project, are walked (one root per plugin even when the file lists the same install twice), and a plugin you have disabled in `~/.claude/settings.json` (`enabledPlugins`) stays disabled here. A project's own `.claude/settings.json` can never re-enable a plugin you disabled. The `.oceancode/` directory in a project is the **local** tier — your machine's private copy, trusted precisely because it is not shared repo content — and is git-ignored in this monorepo. ## Four ecosystems Each tier is read for four ecosystems at once, so a capability written for another tool is picked up without conversion: - **OceanCode** — the native `.oceancode/` layout. - **Claude Code** — `.claude/` (skills, agents, rules, `settings.json`). - **OpenCode** — `.opencode/` (`opencode.json`, agents). - **Grok Build** — `.grok/` (agents, personas, rules, memory). Frontmatter and directory conventions from each ecosystem are translated into the one internal shape as they are read; you do not have to rewrite a Claude Code skill or a Grok persona to use it here. Rule files may pull in other files with `@path` imports (up to four hops); an import is bounded to the project directory and your home directory, so a rule can never inline a file from elsewhere on disk. A rule may scope itself to a file glob in its frontmatter — `paths: "**/*.py"` (Claude Code / OpenCode) or `globs: "**/*.ts"` (a Cursor `.mdc`). A scoped rule is loaded only when the working tree actually contains at least one file matching its glob; in a checkout with no match it is left out entirely, so a TypeScript project does not carry Python or Ruby rule bodies. The check honours your ignore rules (`.gitignore`, `.git` pruned) and runs once per session. Scope today is resolved for the whole checkout, not per individual file the current turn touches. A rule with no `paths`/`globs` is always loaded — with one Cursor exception: a `.mdc` under `.cursor/rules` that says `alwaysApply: false` and has no `globs` is Cursor's agent-requested or manual rule, which Cursor attaches only when asked, and the engine leaves it out too (a `globs:` rule keeps its scope whatever the flag says; the key means nothing outside `.cursor/rules`). ## Workflow scripts Workflow scripts are discovered the same way, from two OceanCode roots in the usual precedence (**built-in > project > user**). Every `*.rhai` file under a root is one workflow, identified by the `meta.name` its script declares — not by its file name: | Scope | Root | Files | |---|---|---| | Project | `.oceancode/workflows` | `*.rhai` | | User | `~/.config/oceancode/workflows` | `*.rhai` | Two workflows ship **built in** to the binary — the highest tier — so they run by name with no `*.rhai` file on disk. Built-ins **cannot be shadowed**: a project or user script with the same `meta.name` as a built-in is ignored (dropped from the picker catalog with a warning), and the built-in still runs — that is what "built-in > project > user, first wins" means. A duplicate `meta.name` across two discovered files likewise keeps the first (project before user). | Name | Args | What it does | |---|---|---| | `deep-research` | `query` (or `objective`); `breadth` 2–6, default 4 | Researches the query with bounded parallelism across four phases — Plan, Research, Verify, Report — independently cross-checks every candidate claim, and writes a cited report. | | `goal` | `objective`; `skeptics` 1–5, default 3; `max_rounds`, default 10; optional `worker_model`, `verifier_model` | Proves an objective is done, not just claimed done: plans it into verifiable deliverables, works in rounds with one resumed worker, then has a panel of adversarial skeptics try to refute completion. Converges only when a strict majority of the cold panel fails to refute; pauses on identical gaps two rounds running or a spent agent budget. | | `pipeline` | `steps: [{ prompt, label?, model?, agent_type?, output_schema? }]`; `stop_on_failure`, default true | Runs an ordered list of agent steps one after another; each step is given the previous step's output. Stops at the first failed step unless `stop_on_failure` is false. | ## Names that collide are qualified; unique names stay bare A capability keeps its bare `name` when that name is unique across everything discovered. Only when the same name appears more than once is it **qualified**, and only the colliding members are rewritten: | Situation | Qualified name | |---|---| | Same name at two different tiers | `tier:name` (e.g. `repo:review`) | | Same tier, different ecosystems | `tier.<ecosystem>:name` (e.g. `repo.claude_code:review`, `repo.open_code:review`) | | A plugin tier | `<plugin>:name` | | A residual same-tier, same-ecosystem tie | `tier.<ecosystem>.<root-dir>:name` | The pass reads only each item's bare name and source, so it is idempotent — running discovery again yields the same qualified names in the same order. The **slash name** of a plugin skill is always `<plugin>:name`, even when the name is unique — Claude Code's convention, so `/superpowers:writing-plans` lists and runs in the Ocean pager's `/` popup exactly as it does there. Typed as a prompt, `/<plugin>:name` and the bare `/name` both run an enabled plugin skill; a `<plugin>:` prefix that names no installed plugin does not resolve (a warning toast; the text is then sent as a normal message). ## Enable is not trust Two independent gates decide what a capability does: - **Enabled** — whether the capability is discovered and offered at all. Disabling a plugin removes its capabilities from the walk — including its rule bodies, which no longer inject into the system prompt (its skills, agents, commands, hooks and MCP servers drop too). - **Trusted** — whether a capability that can *execute code on your behalf* is allowed to run without a prompt. **Only hooks and MCP servers gate on trust**; a skill or a persona is inert data and needs no trust decision. By default, Local, User and Bundled tiers are trusted; the Repo tier is untrusted (code that arrived with a checkout has not been vetted by you); a Plugin is trusted only if you installed it with trust. So enabling a repo-supplied hook is not the same as trusting it — it is discovered, but it will not run its command until you trust it. Trust decisions for a project persist in `~/.oceancode/trust.json`, keyed by the project's absolute path: `repo` (may the repo tier's hooks and `.mcp.json` servers execute?), `mcp` (per-server overrides that beat `repo`) and `disabled_mcp` (servers held disabled at any tier). Plugin trust lives beside the plugin in `~/.oceancode/plugins/installed.json`. Set them with `oceancode trust repo on|off`, `oceancode trust mcp <name> on|off|clear`, `oceancode plugin trust <name> [--off]`, or from the `/hooks`, `/mcp` and `/plugins` pickers; read them with `oceancode trust show`. ## Enabling and disabling skills Loading every skill from every tier costs prompt tokens — one `<available_skills>` line and one `skill_*` tool per skill, plus a disabled plugin's rule bodies. Skills, plugins and MCP servers share **one enable interface** so you can turn off what you are not using; a disabled skill costs nothing (it is not in `<available_skills>`, registers no `skill_*` tool, and its rules do not inject). A skill's effective state is the fold of four tiers, resolved in this order — the first that turns it off wins, so a **project can disable a skill but can never re-enable one you disabled**: | # | Tier | Turned off by | `skill list` reason | |---|---|---|---| | 1 | Plugin | The skill's plugin is disabled (`oceancode plugin disable`) | `disabled (plugin)` | | 2 | User | You disabled the skill (`~/.oceancode/skill-state.json`) | `disabled (user)` | | 3 | Project | `skills.disabled` in `.oceancode/oceancode.json` | `disabled (project)` | | 4 | Allowlist | `skills.mode = "allowlist"` and the skill is not in `skills.enabled` | `not in allowlist` | The per-skill **user** toggle is written by the CLI (or the `/skills` picker) into `~/.oceancode/skill-state.json` — OceanCode's own store, never Claude's `~/.claude/settings.json`. The **project** policy (`mode`, `disabled`, `enabled`) lives in `.oceancode/oceancode.json` under `skills` (see [02 · Configuration](/oceancode/configuration)); default mode `all` keeps every skill a candidate. The project policy is a **default**: an explicit enable of one skill (`Space` on its row, a plugin-group header's `Space`, or `oceancode skill enable <id>`) is also recorded for this project as `enabled_skills` in `~/.oceancode/trust.json` and beats the project's `skills.disabled` and allowlist (tiers 3–4), so a skill the allowlist leaves out can be switched on without editing the config; it never beats the master switch or a disabled plugin, a disable drops it again, and `enable-all` stays user-tier only. MCP keeps its own `disabled_mcp` / `enabled_mcp` model (`oceancode mcp enable|disable`, see [11 · MCP servers](/oceancode/mcp)); the verbs are the same shape. ```bash oceancode skill enable <id> # clear the user-tier disable; beats the project policy oceancode skill disable <id> # disable this skill at the user tier oceancode skill enable-all # clear every user-tier skill disable oceancode skill disable-all # disable every currently-discovered skill ``` `<id>` is a skill's **stable id** `<tier>.<eco>:<name>` — e.g. `user.claude_code:agents-sdk`, `local.ocean_code:tdd`, `repo.open_code:deep` — shown in the `id` column of `skill list`. It is computed the same way whether or not the name collides with another tier, so the store never loses a disable when a session sees only one tier. You may also pass a **bare name** when exactly one discovered skill has it (`oceancode skill disable wrangler`); a name shared by more than one skill is refused with the candidate ids, so pass the full `id` to pick one. A bare name is still accepted in `skills.disabled`/`skills.enabled` and as a legacy store entry (it matches every copy of that name). If you disabled skills before this change, re-run `oceancode skill disable-all` once to rewrite the store with stable ids. A toggle takes effect **on the next step** of the running session. The `/skills` and capabilities pickers, the `/skill` invocation gate and the Ocean pager's composer `/` completion update at once (an enabled skill is offered as `/<skill>`, a disabled one is not); the `<available_skills>` block, the `skill_*` tools, the MCP tool list and the hook dispatcher are rebuilt before the agent's next model call, so the very next request no longer carries what you turned off. A step already running finishes with what it started with. Each change made mid-session costs **one system-prompt cache rewrite** (the cached prefix changes once), so flip several things together rather than one per turn. A change made from a shell (`oceancode skill disable-all`) lands on the next step too. **Whole groups and everything at once.** In the Ocean pager's capabilities picker (`/skills`, `/plugins`, `/hooks`, `/mcps`) `Space` works at four levels: | Row under the cursor | `Space` toggles | Asks first | |---|---|---| | A skill, plugin, hook or MCP server | that one item | no | | A `Plugin: <name>` header (Skills, MCP tabs) or a hook source header (Hooks tab) | every row in the group | above 10 rows | | `All skills` / `All plugins` / `All MCP` / `All hooks` (second row of the tab) | the whole kind | yes | | `Everything` (first row of every tab) | the master switch | yes | `Everything` off removes skills, MCP tools, hooks and plugin rules from the agent's context; the built-in tools (bash, read, edit, …) stay. Turning it back on restores exactly the per-item state you had. A group whose rows are all off shows `[off]` on its header, and the picker's last row shows the live counts — `skills 22/259 · plugins 2/21 · mcp 1/10 · hooks 8/8` — with `EVERYTHING OFF` in front while the master switch is off. Group and master state is per project, stored next to `disabled_mcp` in `~/.oceancode/trust.json`; per-skill disables stay in `skill-state.json`. The Plugins tab lists Claude Code's cached plugins (under `User (Claude)`) beside the OceanCode-store ones, and `Space` turns a whole plugin — its skills, hooks, MCP servers and rules — off or on. **Start from nothing.** To strip the prompt down and add capabilities back deliberately, disable everything and re-enable one by one: `oceancode plugin disable-all` (then re-enable from `/plugins`), and `oceancode skill disable-all` with `skills.mode = "allowlist"` + a short `skills.enabled` list for the handful of skills you actually want. With every plugin disabled and an empty allowlist, the first turn carries no `<available_skills>` block and no `skill_*` tools at all. ## `oceancode inspect` — where did this come from? To see the winning source of every capability, config key, MCP server and instruction file for the current directory: ```bash oceancode inspect # a human-readable provenance table oceancode inspect --json # a stable JSON array (for scripts) ``` Each row names the kind, the id, the layer that won, and the file it came from — the fastest way to answer "why is *this* skill loaded and not that one?". It boots nothing and is read-only. `instruction` rows list what the directory contributes to the system prompt: one primary file — `CLAUDE.md`, else `AGENTS.md`, never both, so a generated `AGENTS.md` mirror is not loaded twice — then `CLAUDE.local.md` and `CONTEXT.md` when present. The same rule applies at every level from the worktree root down to the working directory, and to the reminder that fires when a tool first touches a directory this session. The system prompt is fixed for the rest of a prompt, so an edit to these files (or a URL instruction expiring, or midnight passing) made while a prompt runs applies from the next prompt, not the next step; switching a capability or the model still applies on the very next step. ## Plugins and marketplaces A plugin bundles skills, agents and hooks under one installable tree. Manage them from the CLI: ```bash oceancode plugin marketplace add <spec> # register a marketplace oceancode plugin marketplace list oceancode plugin marketplace update <name> oceancode plugin marketplace remove <name> oceancode plugin install <name-or-path> [--trust] [--marketplace <m>] oceancode plugin list oceancode plugin enable <name> # / disable <name> oceancode plugin enable-all # / disable-all — flip every installed plugin oceancode plugin trust <name> [--off] # grant / revoke trust oceancode plugin validate <name> # check the manifest oceancode plugin update <name> # / uninstall <name> ``` `install` without `--trust` enables the plugin but leaves it untrusted, so its hooks and MCP servers stay dormant until you run `plugin trust`. Pass `--root <dir>` to any `plugin` subcommand to point at a project other than the current directory. In Ocean pager, `/plugins` opens the Plugins tab of the capabilities picker; see [19 · Ocean pager](/oceancode/pager#what-is-wired-to-the-ocean-engine). ## Importing from Claude Code If you already use Claude Code, one command brings its setup here: ```bash oceancode plugin import --from claude-code --dry-run # show what would happen, write nothing oceancode plugin import --from claude-code --trust # do it, trusting the imported plugins ``` What it does, and what it deliberately does not: - **Plugins are copied** from Claude Code's cache (`~/.claude/plugins/cache/<marketplace>/<name>/<version>/`) into `~/.oceancode/plugins` — only the versions installed for the user scope or for this project — and each is enabled or disabled as your `~/.claude/settings.json` says. Without `--trust` they are enabled but untrusted, so their hooks and MCP servers stay dormant. - **Marketplaces** Claude Code knows (`known_marketplaces.json`) are registered. - **Trust is seeded** from Claude Code's own record in `~/.claude.json`: the project's `hasTrustDialogAccepted` becomes `repo=true` (pass `--no-repo-trust` to opt out), servers in `disabledMcpServers` / `disabledMcpjsonServers` are held disabled, `enabledMcpjsonServers` are trusted. - **Skills, commands, agents, rules, hooks and MCP configs at the user and repo tiers are not copied.** `~/.claude/skills`, `~/.claude/commands`, `~/.claude/settings.json`, `.claude/**` and `.mcp.json` are read where they are, so they stay in sync with Claude Code. Every line the command prints names one action (`IMPORT`, `SKIP`, `MISSING`, `MARKETPLACE`, `TRUST`); the last line is `IMPORT_OK plugins=<n> marketplaces=<m> repo_trust=<bool>` (with `--dry-run`, `IMPORT_DRY_RUN_OK …`). A cache tree that is not a plugin — Claude Code's `rust-analyzer-lsp` / `typescript-lsp` entries carry neither a manifest nor a `SKILL.md` — is reported `SKIP <name> not a plugin` and one entry that fails to install is reported `SKIP <name> install failed: <why>`; neither stops the run, so the trust seeding at the end always happens. Nothing under `~/.claude` is ever modified. ## `/skills` — browse and run Every discovered skill — yours, the project's, a plugin's, from any of the four ecosystems — is a slash command: `/<name> <arguments>` runs it as a command turn whose prompt is the skill body prefaced by a one-line instruction, with `$ARGUMENTS` and the positional rules applied. A name that collides across tiers is invoked by its qualified form (`/repo:review`). The completion popup lists skills with the other commands. In Ocean pager, `/skills` opens the Skills tab of the capabilities picker and the composer's `/` completion lists your enabled skills; see [19 · Ocean pager](/oceancode/pager#what-is-wired-to-the-ocean-engine). Without a session: ```bash oceancode skill list # id tier trusted|untrusted <state> description oceancode skill list --json # each row carries "id", "enabled" and a "reason" tag when disabled ``` The `id` column is the stable `<tier>.<eco>:<name>` id (`--json` also keeps the collision-resolved `qualified` name); copy it straight into `oceancode skill enable|disable <id>`. The state column reads `enabled`, or why the skill is off — `disabled (plugin)`, `disabled (user)`, `disabled (project)` or `not in allowlist` (see [Enabling and disabling skills](#enabling-and-disabling-skills)). ## See also - [10 · Hooks](/oceancode/hooks) — the one capability that gates on trust and can change a run. - [11 · MCP servers](/oceancode/mcp) — the other trust-gated capability, and its own translation table. - [19 · Ocean pager](/oceancode/pager) — `/plugins`, `/skills`, `/mcps`, `/hooks` in the terminal UI; the pager's other commands are in `ocean-pager/crates/ocean-pager-ui/docs/user-guide/`. - [07 · Headless and server](/oceancode/headless-and-server#managing-capabilities-from-the-cli) — every management subcommand in one table. # Hooks Source: https://www.plungeai.com/docs/oceancode/hooks A **hook** runs your own command (or an HTTP endpoint) at a fixed point in a run and hands back a typed decision the engine acts on. Hooks are configured in a Claude Code `settings.json` / `hooks.json` shape, so a hook you already wrote for Claude Code runs here unchanged. ## The nine events A hook is registered against one of nine lifecycle events: | Event | Fires | |---|---| | `PreToolUse` | Before a tool runs — can deny it or rewrite its input. | | `PostToolUse` | After a tool returns. | | `Stop` | When a turn is about to settle — can continue the run instead. | | `PermissionDenied` | When a permission check refuses an action. | | `UserPromptSubmit` | When you submit a prompt — can add context. | | `SessionStart` | When a session opens. | | `SessionEnd` | When a session closes. | | `SubagentStop` | When a spawned sub-agent settles. | | `PreTide` | Before the ledger cuts context out of the window (past the tideline). The old event name `PreShelve` still fires it as an alias. | `PreToolUse` and `PostToolUse` carry the `tool_name`, so a handler can scope itself to one tool. ## The `settings.json` shape Handlers live in a `settings.json` (or `hooks.json`) under the same keys Claude Code uses — the event name maps to a list of hook groups, each group a matcher plus one or more handlers: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "./scripts/guard.sh", "timeout": 10 } ] } ] } } ``` A `command` handler is run through `<shell> -c <command>` (shell defaults to `sh`) in the **project directory**, with the event payload as JSON on stdin and these variables on its environment: `OC_HOOK_EVENT` (the event name), `OC_PROJECT_DIR` and `CLAUDE_PROJECT_DIR` (both the project directory, so a Claude Code hook script that reads `$CLAUDE_PROJECT_DIR` works unchanged). An `http` handler POSTs the payload to a URL with your headers. Hooks are discovered as capabilities (see [09 · Capabilities and plugins](/oceancode/capabilities)) and, because they execute on your behalf, they run only from a **trusted** source. The payload is the Claude Code shape: every event object carries `hook_event_name` and `cwd` (inserted only when absent, never overwriting a field the event already set); `SessionStart` carries `source: "startup"`, `SessionEnd` carries `reason: "exit"`. A group's `matcher` is a regex tested against `tool_name` for `PreToolUse` / `PostToolUse` / `PermissionDenied`, against `source` for `SessionStart` and against `reason` for `SessionEnd`; the other events ignore it. An absent or empty matcher matches everything. One documented gap: `session_id` is not available at the hook seam, so a handler that keys on it sees no such field. ## Where hooks are read from — and when they run At boot (and on every reload) `oceancode` walks every ecosystem's hook files — its own `.oceancode/`, Claude Code's `.claude/` and Grok Build's `.grok/` — in this order, and merges their groups per event, so earlier tiers' handlers run first: | Source | Tier | Runs when | |---|---|---| | `.oceancode/hooks.json` in the project | local | always — the local `.oceancode/` tier is your machine's own copy, trusted. | | `.claude/settings.json`, `.claude/settings.local.json` in the project | repo | only when the project is trusted (`oceancode trust repo on`, or `t` in `/hooks`); otherwise **held**. | | `~/.claude/settings.json` | user | always — the user tier is trusted. | | `.grok/hooks.json` in the project, `~/.grok/hooks.json` | repo / user | Grok Build's hook files, gated exactly like the repo and user tiers above. | | `<plugin>/hooks/hooks.json` of an installed plugin | plugin | only when the plugin is enabled **and** trusted; `${CLAUDE_PLUGIN_ROOT}` in its commands is replaced with the plugin's root. | A held source is still parsed and listed — with its handlers — so you can see exactly what trusting it would enable. The reasons are `held: untrusted` (repo tier, project not trusted), `held: disabled plugin`, `unreadable` and `invalid`. Since 2026-09-10 the three Claude Code settings files are also **watched**: saving one reloads the hooks (the same path `r` in `/hooks` and `POST /hooks/reload` take) together with the [Claude Code settings mirror](/oceancode/configuration#claude-code-settings-mirror) — no reload needed after editing `~/.claude/settings.json`. **On the `claude` CLI route there is no double firing.** When a session's provider route is the `claude` command-line protocol, the `claude` subprocess reads the very same `~/.claude/settings.json`, `.claude/settings.json`, `.claude/settings.local.json` and its installed plugins' `hooks/hooks.json`, and runs those hooks itself. On that route `oceancode` therefore dispatches only its own `.oceancode/` hooks (the `ocean_code` ecosystem, local and user tiers) and leaves every Claude Code-shared source to Claude Code — a `UserPromptSubmit` handler in `.claude/settings.local.json` fires once per prompt, not twice. Every other route (Anthropic API, OpenAI-compatible, Grok, …) dispatches everything in the table above, since nothing else would. `/hooks` and `GET /hooks` still list all sources on every route; the by-source report marks the shared ones (`shared_with_claude_code`). ## `/hooks` and `oceancode hooks list` In Ocean pager, `/hooks` opens the Hooks tab of the capabilities picker (toggle and reload go through the engine); see [19 · Ocean pager](/oceancode/pager#what-is-wired-to-the-ocean-engine). Without a session: ```bash oceancode hooks list # one line per source, then its handlers indented oceancode hooks list --json # {"repo_trusted": bool, "sources": [...]} oceancode hooks add PreToolUse --matcher Bash --command ./scripts/guard.sh [--timeout 10] [--scope user|project|local] oceancode hooks remove PreToolUse --matcher Bash --command ./scripts/guard.sh [--scope user|project|local] ``` `list` ends with `HOOKS_OK sources=<n> handlers=<m> loaded=<k>`; `add` prints `HOOK_ADDED <path>` and `remove` prints `HOOK_REMOVED <path>` (`--scope` defaults to `project`; `remove` of a handler that is not there exits 1 with `hooks remove: no such handler`); `--dir <d>` and `--home <h>` select another project or home. The scope names are Claude Code's: **user**, **project** and **local** are the three files. Note that both project files — `.claude/settings.json` and `.claude/settings.local.json` — are listed at tier `repo`: they live in the checkout, so they run only when the project is trusted. (`local` the *scope* is Claude Code's name for the git-ignored project file; `local` the *tier* is `.oceancode/`, your machine's private copy.) Over HTTP a client reads `GET /hooks`, reloads with `POST /hooks/reload`, toggles repo trust with `POST /trust/repo {"on": true|false}`, adds with `POST /hooks {event, matcher, command, timeout?, scope}` and removes with `POST /hooks/remove {path, event, matcher, target}` — every one answers the same `{repo_trusted, sources}` document. ## The decision contract A handler communicates back two ways: its **exit code** and a JSON object on **stdout**. | Signal | Meaning | |---|---| | exit `2` | Deny. On `PreToolUse` the tool does not run; on `Stop` it is a deny that does not count toward the Stop cap. | | any other non-zero, or a spawn / timeout / IO error | No opinion — **fail-open**, the run proceeds. | | stdout `hookSpecificOutput.permissionDecision` | An explicit `allow` / `deny` / `ask`. | | stdout `updatedInput` | Rewrites the tool input before it runs. | | stdout `additionalContext` | Extra text folded into the model's context. | | stdout `stopReason` | On `Stop`, the reason the run should continue rather than settle. | Grok Build handlers may also return `updatedToolOutput` (honoured only for a model-visible copy) and `defer`. Everything but an explicit deny fails open: a handler that crashes, times out or prints garbage never wedges a run. Only `command` and `http` handlers run here, and only the nine events above. A handler of another type — Claude Code's `prompt` and `agent` hooks — or one registered under an event this runner does not implement (Claude Code's `Notification`, `PreCompact`, `SubagentStart`, …) is skipped and counted (`skipped: <n> unsupported handlers` beside the source in `/hooks`), never fatal: the other handlers in the same file still load. ### Continuing a `Stop` A `Stop` handler that wants the run to keep going returns a `stopReason` — the engine treats that as a continuation and drains again. To keep a runaway handler from looping forever, **at most 8 `Stop` continuations** are honoured per run; past that the run settles. An exit-`2` on `Stop` is a plain Deny and does **not** count against those 8. ## Provenance and replay Every hook outcome is recorded with its provenance — which source, which handler, what it decided — so a run can be replayed and audited: you can see exactly which hook denied a tool or continued a turn, and from which tier it came. `oceancode inspect` lists every hook source with `loaded` or `held` beside it. ## See also - [09 · Capabilities and plugins](/oceancode/capabilities) — discovery, tiers, and the trust gate hooks depend on. - [11 · MCP servers](/oceancode/mcp) — the other trust-gated capability. - [02 · Configuration](/oceancode/configuration) — `OC_HOOK_EVENT` and `OC_PROJECT_DIR`, set on every command handler. - [19 · Ocean pager](/oceancode/pager#what-is-wired-to-the-ocean-engine) — `/hooks` in the terminal UI. - [07 · Headless and server](/oceancode/headless-and-server#managing-capabilities-from-the-cli) — `oceancode hooks list` and `oceancode trust`. # MCP servers Source: https://www.plungeai.com/docs/oceancode/mcp An **MCP server** is an external process (or HTTP endpoint) that contributes tools, resources and prompts to a run. `oceancode` reads the same server-config files the other tools use, so a server you configured for Claude Code, Cursor or OpenCode is picked up as-is. ## Where servers are configured Servers are read from several files at once and merged by precedence: | File | Ecosystem | Shape | |---|---|---| | `.mcp.json` | Claude Code (project) | `mcpServers { … }` | | `~/.claude.json` | Claude Code (user) | `mcpServers { … }` | | `.cursor/mcp.json` | Cursor (project) | `mcpServers { … }` | | `opencode.json` (`mcp {}`) | OpenCode | `mcp { … }`, local and remote | | `<plugin>/.mcp.json` | an installed plugin | `mcpServers { … }` | | `oceancode.json` (`mcp {}`) | OceanCode | `mcp { … }` — declared servers win over discovered ones of the same name | **Precedence, highest first: local > project > user > plugin.** A server defined closer to you overrides a same-named server from a wider scope. Every server from every file is folded into the one manager at boot, so `/mcp`, `GET /mcp` and `oceancode mcp list` all show the same set — but a name declared at two tiers runs **once**: the copy that lost its name (qualified `<tier>:<name>`, say `user:cloudflare-dev` for a `~/.claude.json` copy of the project's `.mcp.json` server) is folded **held disabled** — listed, never spawned, its tools never offered a second time under a `user_cloudflare-dev_*` prefix — and `mcp list` reads `shadowed` on it. Only when the winning copy cannot run (held untrusted or disabled) does the next copy run in its place. Before 2026-10-13 both copies were started. ## Held until trusted A server that arrived with a checkout (the project's `.mcp.json`) or with a plugin can run code on your machine, so it is **listed but held** — status `untrusted` — until you trust it. Nothing is dialed or spawned for a held server. Three ways to release one, all persisted in `~/.oceancode/trust.json` under the project's absolute path: ```bash oceancode trust repo on # trust the repo tier: its .mcp.json servers and its hooks oceancode trust mcp <name> on # trust one server (beats the repo flag); `off` holds it again; `clear` removes the override oceancode trust show # repo=<bool>, one `mcp <name>=<bool>` per override, one `disabled <name>` per held-disabled server, then TRUST_OK ``` or trust it from the pager's `/mcps` tab. Trust pins what was approved: the first time a trusted repo or plugin server is seen (or when you trust it), its command (stdio) or URL is recorded as a hash in `trust.json` (`mcp_hashes`). If the checkout or plugin later changes that command or URL, the server is held `untrusted` again until you re-approve it with `oceancode trust mcp <name> on` or `t` in `/mcps`. Environment and headers are not pinned. Once it runs, each of its calls still passes the `mcp` permission rules and asks by default ([12 · Permissions](/oceancode/permissions-and-sandbox#mcp-calls--the-mcp-key)). A server Claude Code has switched off for this project (`disabledMcpServers` / `disabledMcpjsonServers` in `~/.claude.json`, seeded by `oceancode plugin import --from claude-code`) is held **disabled** at every tier and reports `disabled`. User-tier servers (`~/.claude.json`) are trusted by default. Enabled and trusted are independent: `oceancode mcp disable <name>` (or the pager's `/mcps` tab) switches a server off — disconnecting it and recording the hold in `trust.json` — without changing its trust, and `enable` switches it back on; a server that is enabled but still untrusted stays held and reports `untrusted`, and `connect` refuses to dial it until it is trusted. The engine reads a `trust.json` change made from the CLI at its next boot; the pager and the HTTP routes apply it live. `oceancode mcp enable|disable` is the MCP arm of the one enable interface skills, plugins and MCP servers share (`skill enable|disable`, `plugin enable|disable`, and `enable-all`/`disable-all` on each) — see [Enabling and disabling skills](/oceancode/capabilities#enabling-and-disabling-skills). MCP keeps its own `disabled_mcp` hold model rather than the skills' four-tier fold; the verbs are the same shape so there is one thing to learn. An explicit enable is remembered per project as `enabled_mcp` in `~/.oceancode/trust.json` and beats a config entry's `enabled: false`, so a server your project config ships off can be switched on from the picker or the CLI and stays on. Any `${VAR}` or `${VAR:-default}` in a server's command, args, env or headers is expanded from your environment when the server starts; an unresolved `${VAR}` is left literal rather than blanked, so a misconfigured variable is visible instead of silently empty. Like hooks, MCP servers execute on your behalf, so they run only from a **trusted** source (see [09 · Capabilities and plugins](/oceancode/capabilities#enable-is-not-trust)). ## Resources, prompts and elicitation An MCP server offers more than tools: - **Resources** — the server's `resources/list` and `resources/read` are surfaced so a server can hand documents into a run. - **Prompts** — each server prompt appears as a slash command named `/mcp__<server>__<prompt>`. - **Elicitation** — a server that asks the user a question mid-call is routed to the same `question` seam the rest of the app uses, so the prompt shows up in your UI and the answer goes back to the server. ## Meta-dispatch for large tool sets When many servers are connected, the flat tool catalog gets unwieldy for the model — and some providers reject a `tools` array longer than 128. Past 128 connected MCP tools the engine always replaces the flat catalog with two meta-tools — `search_tool` (find a tool by keyword) and `use_tool` (invoke one by id). Setting `OC_MCP_META_TOOLS=1` does the same from 64 tools up. See [02 · Configuration](/oceancode/configuration). ## Managing servers — `oceancode mcp` ```bash # A stdio server (a local command): oceancode mcp add <name> --command <cmd> [--arg <a>]... [--env K=V]... # A remote server (an HTTP endpoint): oceancode mcp add <name> --url <url> [--header K=V]... oceancode mcp list # name stdio|remote tier trusted|untrusted enabled|disabled|shadowed source oceancode mcp enable <name> # / disable <name> — switch any server off or on (config-declared or discovered; held disabled in ~/.oceancode/trust.json, honoured at boot) oceancode mcp remove <name> oceancode mcp login <name> # / logout <name> — OAuth for a server that needs it oceancode mcp doctor # probe every server and report reachable / failed (a shadowed copy is not dialed; it reads shadowed) ``` `list` walks every config file above for the current directory (`--dir <d>`, `--home <h>` to point elsewhere) and shows, per server, its tier, whether it may run and whether it is enabled. `add` writes a Claude-Code-shaped `.mcp.json`, so the server is immediately visible to any tool that reads that file. Pass exactly one of `--command` (stdio) or `--url` (remote). `--root <dir>` and `--home <dir>` override where the config and home are read. `doctor` gives each server a short connect budget and reports it `reachable` or `failed` quickly, so one unreachable server does not hang the check. In Ocean pager, `/mcps` opens the MCP tab of the capabilities picker (add, enable/disable and trust go through the engine); see [19 · Ocean pager](/oceancode/pager#what-is-wired-to-the-ocean-engine). A server whose handshake is still in flight reads **`connecting`** (the status list is `connected` / `connecting` / `needs-auth` / `failed` / `disabled` / `untrusted`); a status read gives the startup pass at most two seconds and then answers with the live rows (the handshake itself may take up to 30 s, the `timeout` default). Disconnecting, disabling or untrusting a server is bounded to about two seconds even when the server process left a child holding its output pipe open. A prompt's first model request waits up to `OC_MCP_WAIT_SECONDS` (default 5; `0` never waits; sub-agents never wait) for the servers configured at boot to finish connecting, so their tools are already in that request. The tool list is then fixed for the rest of the prompt: a server that connects, or lists new tools (`tools/list_changed`), while a prompt runs reaches the model on the next prompt, because a changed tool list at the front of the request would make the provider re-write its whole prompt cache. Toggling a server, connecting or disconnecting it by hand, trust or any capability still applies on the very next step. Over HTTP a client reads `GET /mcp` (each entry carries `trusted`, `enabled`, `provenance` and `kind` beside `status`), `GET /mcp/{name}/tools`, `GET /mcp/{name}/prompts`, `GET /mcp/{name}/resources`, and posts `POST /mcp/{name}/trust {"on"}`, `POST /mcp/{name}/enable {"on"}`, `POST /mcp/{name}/connect|disconnect`, `POST /mcp/{name}/auth` and `DELETE /mcp/{name}/auth`. ## See also - [09 · Capabilities and plugins](/oceancode/capabilities) — discovery, tiers, and the trust gate. - [10 · Hooks](/oceancode/hooks) — the other capability that runs your code. - [02 · Configuration](/oceancode/configuration) — `OC_MCP_META_TOOLS` and the `mcp` config key. - [07 · Headless and server](/oceancode/headless-and-server#managing-capabilities-from-the-cli) — `oceancode trust` and `oceancode mcp list`. # Permissions and sandbox Source: https://www.plungeai.com/docs/oceancode/permissions-and-sandbox OceanCode decides what a tool may do in three layers, from softest to hardest: 1. **Permission rules and modes** — an ask/allow/deny policy over each tool call. 2. **The shell and filesystem guards** — a classifier that reasons about what a `bash` command or a file edit actually does, on top of the rules. 3. **The kernel sandbox** — OS-level isolation the process cannot talk its way out of, off by default. An empty config is not an asking one: the base rule is `* * allow`, so a tool call is allowed unless something fences it — the shell's dangerous floor, an `external_directory` ask, a denied `.env` read. Secret-named environment variables are stripped from child processes and `.env` files are never read into the model. Add `bash * ask` under `permission` to make every shell command off the read-only safe list ask; that is also the posture the Jev permission advisor works on. This page is how to loosen or tighten each layer. ## Permission modes A **mode** is a coarse posture layered over your `permission` rules. Set the boot default with `permission_mode` in the config (see [02 · Configuration](/oceancode/configuration)), or switch it live over the HTTP API (see [Switching the mode live](#switching-the-mode-live) below). | Mode | What it does | |---|---| | _(empty)_ / `default` | Evaluate the rules as written; with the base `* * allow` rule a call no rule fences is allowed. | | `acceptEdits` | Auto-allow `edit`; everything else is unchanged. | | `dontAsk` | Never block on an interactive ask (a residual ask resolves to deny in a headless posture). | | `bypassPermissions` | Auto-allow everything — **except** the dangerous floor below, which no mode can reopen. | The Ocean pager's `auto` (the Shift+Tab Auto stop, `/auto`) is `acceptEdits` and its `always-approve` (`ctrl+o`, `/always-approve`) is `bypassPermissions`; the pager sends every change to the engine (see [19 · Ocean pager](/oceancode/pager#shifttab-ring-and-permission-modes)). A mode is expressed as ordinary last-match-wins rules, so it can never silently override the safety floor: `bypassPermissions` appends an allow-all rule, but the dangerous-floor deny is evaluated first and wins regardless. **Jev advisor.** With the opt-in Jev decision layer on (see [13 · Jev decisions](/oceancode/jev-decisions)), a residual ask on a `bash` command — one that no allow, deny or specific-pattern `ask` rule settled — is first put to Jev as four bounded questions (in `act` the prompt waits at most `jev.deadline_ms`, 1.5 s by default). With the empty config's `* * allow` base there is no such ask, so the advisor sees nothing until a rule such as `bash * ask` makes the shell guard ask. In `shadow`, the default once the layer is enabled, the call runs in the background, its answer is only recorded and you are asked as before, without waiting. With `jev.permissions = "act"` a command inside a bounded safe class — not destructive, no network egress, easily undone, a single-file or project blast radius — runs without the prompt. The dangerous floor and deny rules run before the advisor and never reach it, `dontAsk` gets no advice, a command that names a path outside the project (or runs in a `workdir` outside it) gets none either — the shell guard checks that in code — and a fixed hold list (package installs and publishes, `git push`, recursive `rm`, `ssh`, an uploading `curl`, an interpreter run on a file, …) always asks. Under headless `--auto`, an `act` advice that the command is destructive, sends data out, reaches the system or a third party, or tripped the injection guard rejects the call instead of approving it, and the rejection carries the reason and the Jev scores back to the model as feedback (see [13](/oceancode/jev-decisions#what-jev-decides)). Your answer to an advised prompt is recorded as that decision's label. ### Rule syntax — `Tool(pattern)` Individual `permission` entries and the `shell.allow` / `shell.deny` lists take a `Tool(pattern)` form, e.g. `Bash(git push:*)`, `Edit(src/**)`, `Read(*.md)`. The tool name is matched case-insensitively; an empty pattern means `*`. A tool whose **last** rule for its key is a `*` deny — `bash * deny`, or a read-only agent's blanket deny — is not offered to the model at all: its schema is left out of the request (`write`, `apply_patch` and `hashline` count as `edit`). A narrower last rule keeps the tool, so the plan agent still sees `edit` for its plan file. Sub-agent children are sent the built-in tools only, never MCP or skill tools. ### MCP calls — the `mcp` key Every MCP tool call passes the rules under the key `mcp` with the pattern `<server>/<tool>`, e.g. `"mcp": {"github/*": "allow", "github/delete_repo": "deny"}`. When no `mcp` rule decides, the server's origin does: a server declared in config (the project's `.oceancode/oceancode.json` or your global `oceancode.json`, a user-tier `~/.claude.json`, the project-local tier) or passed by the client (the servers Xcode passes) runs without asking; a server from the project's `.mcp.json`, a plugin or a runtime add (`POST /mcp`, the pager's `/mcp add`) asks, and "always" approves the whole server. Config servers are not trust-gated, including those in a `.oceancode/oceancode.json` committed to a checkout: read that file before you open a repository you don't trust. The server's own `readOnlyHint` is never used: the server supplies it. A headless `oceancode run` without `--auto` cannot ask, so it refuses the call and tells the model the rule that approves the server. Trust is the separate gate that decides whether such a server runs at all ([11 · MCP servers](/oceancode/mcp#held-until-trusted)). ### Switching the mode live The permission mode is switched at runtime over the HTTP API — `GET`/`POST /api/session/{id}/permission/mode` (see [07 · Headless and server](/oceancode/headless-and-server#permission-mode)). A managed (enterprise) requirements file can **lock out** `bypassPermissions`: once locked, selecting it downgrades to `default`. ## The shell guard Before any `bash` command runs, a classifier splits it into segments, peels wrappers (`sudo`, `env`, `nice`, `timeout`, `xargs`, `bash -c "…"`, `VAR=val …`), and decides allow / ask / deny per segment. The whole command runs only if **every** segment is allowed (a chaining guard — `git status && rm x` still asks). - **Read-only safe list.** Commands that only read (`git status`, `git log`, `ls`, `rg`, `grep`, `find`, …) auto-approve with no rule. The list looks at flags: `find` with `-delete`, `-exec`, `-execdir`, `-ok`, `-okdir`, `-fprint`, `-fprint0`, `-fprintf` or `-fls`; `git branch` that deletes, renames, copies, forces or re-points (`-d`, `-D`, `-m`, `-M`, `-c`, `-C`, `-f`, `-u`, `--delete`, `--set-upstream-to`, `--unset-upstream`); `git remote` with `add`, `remove`, `rm`, `rename`, `set-url`, `set-head`, `set-branches`, `prune` or `update`; and `git tag` that creates, signs, forces or deletes (a name after `tag` creates a tag unless `-l`/`--list` makes it a pattern) are off the list and ask. Adding output redirection (`> file`) drops a command off the safe list. - **Dangerous floor.** A fixed set of destructive shapes is refused outright and **no permission mode can reopen it**: `rm -rf /` (and obfuscated variants), `git push --force`, `dd of=/dev/*`, `chmod -R 777 /`, `mkfs*`, the fork bomb, and `curl … | sh`. The floor peels wrappers and unwraps `bash -c`/command substitutions, so hiding the command behind `sudo`, `/bin/rm`, or `$(…)` does not evade it. - **`shell` overrides.** `safe_list_extra` adds read-only commands; `allow` / `deny` layer `Tool(pattern)` rules onto the classifier. Neither can remove the dangerous floor. ## Child-process environment Every `bash` and PTY child gets a composed environment rather than a raw copy of the parent's. By default, variables whose name contains `KEY`, `SECRET`, or `TOKEN` (case-insensitive) are **stripped**, so a secret in your shell never leaks into a spawned tool. Tune it with `child_env`: | Field | Meaning | |---|---| | `inherit` | `all` (default), `core` (only `PATH`/`HOME`/`USER`/`SHELL`/`LANG`/`TERM`/`TMPDIR`), or `none`. | | `include_only` | When non-empty, keep only these variable names. | | `set` | Extra `name`→`value` entries overlaid last. | | `strip_secrets` | Drop secret-named variables (default `true`). | ## The sandbox The sandbox confines a spawned child at the OS level — `sandbox-exec` (Seatbelt) on macOS, `bubblewrap` (with Landlock where the kernel supports it) on Linux ≥ 5.13. It is **off by default**; turn it on with the `sandbox` config. | Profile | Reads | Writes | Network | |---|---|---|---| | `off` | unrestricted | unrestricted | unrestricted | | `workspace` | workspace + writable roots | workspace + writable roots | per `network` | | `readonly` | workspace + writable roots | denied | per `network` | | `strict` | workspace only | workspace only | per `network` | - `deny` lists roots refused even inside the workspace (e.g. `.git`, a secrets dir). - `network` is `off` (no egress) or `allowlist`. **The allow-list is not enforced yet:** on macOS and Linux an `allowlist` policy leaves egress open, because no child's traffic is routed through the TLS-SNI relay. Use `off` to block egress. On a Linux kernel older than 5.13, Landlock is unavailable and bubblewrap alone provides the confinement (a warning is logged, never a silent disable). ## Reference aliases `refs` maps a short `@name` to a directory. A path of the form `@name/rest` in any file tool (`read` / `edit` / `write`) expands to `<refs[name]>/rest`, and that reference root is auto-allowed through the external-directory prompt — so a shared docs or vendor tree can be reached without a prompt for every file. An unknown `@name` is left literal and still prompts, and an alias that walks out of its root (`@name/../escape`) is caught by the external-directory boundary just like any other outside path. ## Filesystem guards The file tools carry their own safety, independent of the rules: - **`.env` is never read.** `read` refuses `.env` and `*.env` / `*.env.*` files (secrets); `.env.example` still reads. - **Read-before-write.** `write` refuses to overwrite an existing file you have not read this session ("file exists but has not been read this session"); a brand-new file writes freely. One exception (2026-09-28): a file under 64 KiB that this session itself created — its birth time is at or after the session's first tool call in this process, e.g. a file a `bash` command generated — can be overwritten without a read. A file that existed before the session is refused even when it was edited since, and on a filesystem that reports no birth time the exception never applies. - **Stale detection.** `edit` and `write` refuse a target that changed on disk since it was last read — detected by content hash, so a same-size, same-mtime change is still caught. Read it again first. Other files changing — a build, a file a `bash` command wrote, the session's own database — never cost you a read: a read stays valid until the file itself changes (2026-09-28). ## Managed (enterprise) config An administrator can layer a **managed** config over every project. It is loaded last, so `project < user < managed` — managed always wins. Its location is the OS admin path, or `$OC_MANAGED_CONFIG` (see [02 · Configuration](/oceancode/configuration)). Alongside it, a signed `requirements.json` carries hard requirements — disabling `bypassPermissions`, an MCP-server allow-list, and config pins. It is verified with ed25519 against the trusted key in `$OC_REQUIREMENTS_PUBKEY`: an **unsigned or tampered** requirements file is refused, never applied. With no key configured, managed requirements are simply not enforced. - `oceancode inspect` shows the provenance of every effective config key — which layer (project / user / managed / requirements) set it. - `oceancode doctor` reports crash recovery state: orphaned sessions and captured panic reports. See [07 · Headless and server](/oceancode/headless-and-server). ## See also - [02 · Configuration](/oceancode/configuration) — the config keys and variables. - [07 · Headless and server](/oceancode/headless-and-server) — `oceancode doctor` and the permission-mode routes. # Jev decisions Source: https://www.plungeai.com/docs/oceancode/jev-decisions OceanCode can put small typed questions to **TypeSafe Jev** (System One, pinned model `jev-1.13.0`) at ten decision points — "would this command delete data?", "which block of this output answers the step?" — and use the answers as advice. Jev answers with probabilities: a `noul` (the probability of yes), a `choice` over named options, or a `score` over ordered levels. The policy that turns an answer into a verdict is code beside each decision point, and the engine's deterministic backstops stay in front of it: Jev can never turn a `deny` into anything, never reopen the dangerous floor, never raise a spend, step or clock bound. The layer is **on whenever a TypeSafe key resolves** (since 2026-10-22 — the native TypeSafe provider: OceanCode calls `https://api.typesafe.ai/v1/systemone` itself, over HTTPS, with the key you store once). Without a key it is off and `oceancode jev status` says so. Four points act by default — the ones a pre-registered benchmark cleared — plus `work_mode`, which is asked only when you pick the Auto work mode; every other point is off until you set its key. An opt-out in either config section, or `OC_JEV=off`, turns the whole layer off. ## What Jev decides | Point · mode key | Question asked | What `act` changes | What stays deterministic | |---|---|---|---| | `permission` · `jev.permissions` | For the shell rules' residual **ask** on a `bash` command: `destructive`, `egress` (any network destination), `reversible` (score 0–2) and `blast_radius` (single_file / project / system / external), in one request. Whether the command reaches outside the project is not asked: the shell guard checks it in code (see below). | A would-ask command runs without a prompt only when destructive < 0.6, egress < 0.5, reversible < 1.5 and the blast is single_file or project with confidence ≥ 0.8. Under headless `--auto` an ask whose advice is an injection refusal, a system/external blast, destructive ≥ 0.6 or egress ≥ 0.5 is **rejected** instead of approved, and the model reads why: the rejection reaches it as feedback — `Blocked under --auto by the permission policy: <likely destructive \| likely network egress \| blast system \| blast external \| the command text addresses the judge>. Jev scores: destructive=<d> egress=<e> blast=<b>. Use a safer equivalent that stays inside the project, or stop and explain why this command is required.` (a score Jev did not give prints `n/a`) — instead of a bare "rejected"; the same reason still goes to stderr. | Deny rules and the dangerous floor run first and never reach Jev. An `ask` rule with a specific pattern keeps its prompt; every other permission kind (paths, `edit`, `write`, …) asks as before; `dontAsk` gets no advice. A command over 4,000 chars, a truncated task or a command that names a path outside the project (skip reason `a path outside the project`; see [What is sent](#what-is-sent)) gives no advice. The **hold** keeps these shapes at ask whatever Jev says: package install/add/publish verbs, `npx`/`bunx`/`uvx`, `go run`; the git verbs `push`/`remote`/`restore`/`clean`/`stash`/`checkout --` and every history rewrite (`reset` in any form, `rebase`, `merge`, `branch -d`/`-D`, `tag -d`, `update-ref`, `reflog`, `worktree remove`/`prune`, `commit --amend`, `filter-branch`, `filter-repo` — plain `commit` is still advised); a `git -c` that sets an alias, a program (`core.sshCommand`, `core.pager`, `core.editor`, `core.fsmonitor`, `core.hooksPath`, `core.gitProxy`, `sequence.editor`), `credential.*`, `http.*` or `url.*`, or any value with `!`; recursive `rm`, `find -delete`/`-exec`, `ssh`/`scp`/`rsync`, an uploading `curl`/`wget`; anything that runs code Jev never sees — a path as the command word (`./deploy.sh`, `scripts/x.sh`, `/abs/tool`), a shell without `-c` (on a file, `-s`, or fed from a pipe or `<`), an interpreter (`python`, `node`, `ruby`, `perl`, `deno`, `bun`, `tsx`, `php`) on a file, an inline program or stdin (only `python -m <module>` and `--version` pass), `source`/`.`/`eval`/`exec`, `awk -f`, `sed -f`; and any opaque or non-ASCII command. | | `output_view` · `jev.output_view` | For a `bash` output over the truncation limits: which of its ≤ 255 line blocks best answers the step (`where`, a choice over block ids) and whether any does (`exists`). The step is the assistant text before the call; when the model chained the call with no text before it, the call's own `description` stands in (the decision row's state then carries `intent_source: "description"`). | The model sees the first 20 and last 20 lines, the blocks Jev picked (most probable first until 0.8 of the mass or 8 blocks, each at most 200 lines) and one `[… lines a–b omitted; full output: <spill>]` marker per gap; the full output goes to a spill file. When `exists` < 0.5 it gets the registry's head preview and the spill file instead — except that the top block is still kept when its probability is at least 0.8 and `exists` is at least 0.2. | Only `bash` is viewed (`read`, `grep` and `glob` page themselves). Binary output, a command whose last stage already filters its output (`grep`, `rg`, `head`, `tail`, `wc`, `sort`, `uniq`, `cut`, `awk`, `sed`, `jq`; skip reason `command already filters`), neither step text nor a `description` of at least 12 characters (skip reasons `no step intent`, `step intent too short`), an output the head and tail already cover, a view that is not smaller than the registry's cut it would replace and a state that does not fit 48,000 bytes keep today's result — for `bash`, as for every tool, the registry's cut: the head preview (at most 2,000 lines / 50 KiB) and the spill file. The head, the tail and the spill file are always there when a view acts. | | `supervisor` · `jev.supervisor` | At a clean depth-0 finish, over the goal, the step count, the open todos, the last six steps and the final text: `complete` and `progress` (score 0–2). Before a depth-0 `bash` call whose exact command already ran at least twice among the prompt's last six commands, the **no-progress** check asks one noul, `proposed_command`: "Would running `proposed_command` give the agent information or progress that it does not already have from `recent_commands` and their outputs?" | The stop is blocked with a synthesized continuation when complete < 0.3, progress ≥ 1.0 and the progress mass ≥ 0.5 (at most `jev.supervisor_max_continuations` per human prompt, default one; a further one is asked about only after a state-changing call since the last); complete ≥ 0.85 always finishes, and so does every stop after a `check` verified a pass this prompt (skip row `a check verified a pass this prompt`, no call). A no-progress answer ≤ 0.31 is a **low**: two consecutive lows send one **steer** message before the next model call; four stop the run — under headless `--auto` the call is not run and the run ends (stop reason `no_progress`, exit 5) with the final text `Stopped: the last 4 commands repeated without new information.`; everywhere else you are asked *Continue* or *Stop* (a dismissed question, or a surface with no question path, stops). Any other answer, a failed call and a command that is not such a repeat reset the count. | The hard step, spend and clock bounds, the exact-hash doom-loop trip, the eight-block hook cap and the `no_progress` guard ([08 · Bounding a long run](/oceancode/bounding-a-long-run)). A stop that asks you something — the final text's last line ends with `?`, or the turn used the `question` tool — is never judged (a skip row, `asks the user`), and neither is a stop after a tool call the permission policy refused among the last six steps — you rejected it, or a permission rule denied it (`a tool call was rejected by policy`): the refusal is a decision, so continuing would only make the agent retry it. A stop after the cap, or a further one with no `edit` / `write` / `apply_patch` / `bash` call since the continuation, is skipped too (`continuation cap reached`, `no state-changing call since the continuation`). | | `check` · `jev.check` | After a depth-0 `bash` call that ran an allow-listed test/build runner (the `triage` list below) after a project-file write this prompt: which of six categories the recorded output supports — `successful_check`, a choice over `new_failure_evidence`, `check_now_passes`, `same_blocker_after_different_attempt`, `repeated_operation`, `other_observable_activity`, `insufficient_evidence` — and, in the same request, `remaining_checks`: does the goal name a check (a test suite, a golden comparison, a build, a minimum number of tests) that neither this command nor an earlier one has run and shown passing. Jev reads the recorded output and the goal; it is never asked to predict whether a change will pass. | `check_now_passes` as the top category with p ≥ 0.5 **and** `remaining_checks` < 0.5 is a **verified pass** (at most one per prompt); a green check while the goal still names an unrun one is not (the pilot's `py-scheduler-refactor`: a clean golden diff before the eight-test suite existed). Under headless `--auto` the run ends right after that call, with no further model request, and the final text `` Checks passed: `<command>` (exit 0). <N> file(s) changed: <paths>. ``; everywhere else the agent carries on and only the supervisor's continuation is withheld for the rest of the prompt. | Jev is not asked — a skip row says why — when the command exited non-zero (`the command exited non-zero`), when every file edited this prompt is a test, spec, snapshot, golden or generated file (`no source file changed this prompt`), or when the prompt already has a verified pass. `shadow` records the verdict (`verified` / `not_verified`) and never stops a run. | | `triage` · `jev.triage` | After a `bash` exit other than 0 and −1: the failure `category` (env_missing / flaky_transient / syntax_trivial / expected_failure / deep_logic). | With category confidence ≥ 0.8: a `flaky_transient` failure of an allow-listed test or build runner is re-run **once** (its output appended after the first run's; `metadata.exit` keeps the first exit code, `metadata.rerun_exit` holds the second); `flaky_transient` elsewhere or `env_missing` adds one hint line. A failure that names something absent (exit 127, `<name>: command not found`, `No module named '<name>'`, `error[E0463]: can't find crate for`, `Cannot find module '<name>'`) is `env_missing` without a request: the hint names it, points at an alternative that is on `PATH` (`pytest` → `python3 -m unittest`, `jest` / `vitest` → `node --test`, `cargo-nextest` / `nextest` → `cargo test`) and tells the model to use what the project has and not to install anything the task did not ask for; an `env_missing` answer from Jev itself gets the same wording with "a command" for the name and never says to install; a failing install command (`pip install`, `python3 -m pip install`, `npm i`, `cargo install`, `go get`, … in any segment of the command) gets no hint. | The raw output is never removed; the re-run asks the permission gate again; a green, timed-out or killed run never calls Jev. The runner allow-list is exact: `cargo test`, `cargo nextest run`, `cargo check`, `cargo clippy`, `cargo build`, `pytest`, `python -m pytest`, `python3 -m pytest`, `go test`, `npm test`, `npm run test`, `pnpm test`, `yarn test`, `vitest`, `jest`, `./check.sh`, optionally after one `cd <relative dir> &&`, with no other separator, pipe, redirect or substitution and no leading `VAR=` other than `RUST_LOG`, `RUST_BACKTRACE`, `NO_COLOR`, `CI`. | | `visibility` · `jev.visibility` | At the ledger drain's prune: which prunable completed-turn tool parts the remaining work still needs (`needed`, a choice over call ids) and whether it needs any (`any_needed`), over the goal and the open todos. | When `any_needed` ≥ 0.4, up to three parts with p ≥ 0.05 are kept verbatim out of the prune — provided the rest still meets the prune minimum. A part kept once stays kept while the plan moves; a part already stubbed is never offered again. The plan's stubs land on the next cut's request only (a prune rides a cut, 2026-10-12); between cuts no sent row changes. | The tideline cut, the protect window, the emergency plan and the byte-stable prefix. `off`, `shadow` and a failure protect nothing. | | `guidance` · `jev.guidance` | After a depth-0 tool result settles in a run that has **stalled** (3 exact repeats among the last 6 commands with no write), has an **edit no check followed**, or has had the relevant code open for 16 steps without an edit: four nouls in one request — `code_located`, `issue_reproduced`, `post_edit_check`, `stalled_repair`. Each is yes at ≥ 0.7, no at ≤ 0.3, unresolved between. | A rule table maps the answers to one action kind — `edit`, `probe`, `search` or `test` — and appends **one line** to the tool result: `Guidance: <reason>; consider <action>.` A note goes out only after 2 consecutive qualifying evaluations, at most 2 per prompt, at least 10 settled calls apart, never after a stop. | The model still picks every command and its arguments. A suggestion of the kind the call just made is dropped (a stall's always qualifies); a failed evaluation resets the count. Each delivered note gets a row and, once resolved, an **uptake** row: did a call of the suggested kind follow within 3 settled calls. | | `snippets` · `jev.snippets` | Once per depth-0 human prompt, before its first model call: deterministic retrieval proposes up to 255 source regions, and one noul per region asks whether it directly bears on the prompt — batched into concurrent requests of at most 128 questions and 38,000 state bytes. | The answered regions are ranked by probability and packed greedily into 12,000 chars; the model call waits for the requests and the pack becomes one message joined onto the prompt's opener, pinned for the whole prompt (later steps send the same bytes; the prune never touches it). | No threshold. `shadow` sends the same requests in the background and logs the would-be pack; `off` runs nothing, not even retrieval. | | `effort` · `jev.effort` | Once per depth-0 human prompt, before its first model call: `difficulty`, a score over four levels (trivial / routine / involved / hard), and `smaller_model_ok`, a noul recorded for tuning and never read. | At confidence ≥ 0.8 the rounded score steps the reasoning effort down from the session's resolved variant — trivial two rungs, routine one, involved and hard none — along the model's own variant ladder, for this prompt only. | Never below `medium` (else the lowest rung above it, else the middle rung); a ceiling at or below the floor is never asked about. Sub-agent turns and the ledger's merge provider are never touched. | | `work_mode` · `jev.work_mode` | Once per top-level prompt of a session that runs the Auto work mode ([23](/oceancode/work-modes#auto)): `mode`, a choice over the work modes that can run here (every picker mode but Auto; no Race outside git or without a `test_command`, no Pipeline or Race without someone to ask, no script mode with workflows off), each named by its summary. | At confidence ≥ 0.6 Jev's pick runs for this prompt, and the badge reads `auto → <pick> · Jev <confidence>`. | Below the threshold, with the point `off`, without a key, or on an error or time-out, the rules pick from the same list (`auto → <pick> · rules: <reason>`). Never asked outside Auto; `shadow` runs the rules' pick and logs Jev's. | ## Turning it on 1. **A TypeSafe API key.** Get one in the [TypeSafe console](https://console.typesafe.ai) and store it where every other provider key lives (`auth.json` in the data dir): ```bash oceancode key typesafe <key> # or pipe it: echo "$KEY" | oceancode key typesafe oceancode key # reports it: typesafe : API key stored (auth.json) — the Jev decision layer is on. oceancode logout typesafe # removes it ``` The environment works too: `OC_TYPESAFE_API_KEY`, else `TYPESAFE_API_KEY` (the name TypeSafe's own SDKs read). The stored key wins over both. The key goes only to `api.typesafe.ai` — never to another endpoint, never into a config file, never to a child process. 2. **That is all.** No `jev` section is needed: with a key the layer is on with the defaults below. A `jev` section in the global config (`$OC_CONFIG`, else `<config-dir>/oceancode/oceancode.json`, or the managed document an administrator ships, which wins over it) sets the endpoint, model, budgets and thresholds; a project section (`<project>/.oceancode/oceancode.jsonc` or `.json`) may set only `enabled` and the ten mode keys — its `endpoint`, `model`, budgets and thresholds are ignored with one warning, so a cloned repository can neither send your states or credentials elsewhere nor loosen a threshold. Either section can opt out: ```json { "jev": { "enabled": false } } ``` 3. **The endpoint.** The default is TypeSafe's own API, `https://api.typesafe.ai/v1/systemone` (`model` defaults to the pinned `jev-1.13.0`; the thresholds are tuned on it). `jev.endpoint` in the global config or `OC_JEV_ENDPOINT` names another: the PlungeAI worker (the hosted typesafe provider, which needs no TypeSafe key and takes the `ocean.headers` map of the global config — the managed document or yours, never the project file's — as `x-ocean-key`), or a loopback stub. The key goes only to `api.typesafe.ai`, `ocean.headers` only to the worker, a redirect is never followed, and plain `http` is refused unless `OC_JEV_ENDPOINT` names a loopback host. Every request names its decision point in an `x-jev-point` header and the direct API's `Server-Timing` (when present) is kept on the decision row. 4. **Check it.** `oceancode jev status` lists the ten points with their modes and the endpoint host (`api.typesafe.ai`); `oceancode jev probe` makes one live call and prints `JEV_PROBE_OK`. Without a key, `status` prints every point `off` and `probe` names the reason: `no TypeSafe API key: run \`oceancode key typesafe <key>\` …`. ## Modes Each point has its own key: `off`, `shadow` or `act`. | Mode | What the point does | |---|---| | `off` | Never calls Jev; today's behaviour, no decision row. | | `shadow` | Asks Jev, records the verdict it would have given, and changes nothing. The call runs in the background, so the turn never waits for it; its row is written when the answer lands, and every row is in `jev-decisions.jsonl` by the time the session closes. | | `act` | Asks Jev and applies the verdict inside the bounds of [What Jev decides](#what-jev-decides). | A point whose key is unset runs in its **default mode**: `act` for the four points the 2026-10-22 pre-registered benchmark cleared — `check`, `guidance`, `snippets`, `effort` (165 runs, 11 jobs × 5 arms × 3 reps, no arm RED, grade +3.0 pp [+0.0, +9.1], wall −17 to −19 % on the arms that add them; `docs/results/2026-10-21-jev-rebuild/README.md`) — and `off` for every other point: the supervisor (its no-progress check never fired in 132 Jev-arm runs; owner ruling 2026-10-22) and the four label-only points (`permission`, `output_view`, `triage`, `visibility`). `work_mode` also defaults to `act`, but it is asked only for a session that runs the Auto work mode ([23](/oceancode/work-modes#auto)), so every other session never calls it. Nothing runs in `shadow` by default: a shadow call is a Jev call the run never acts on, so you set a key to `shadow` when you want a label run. A point's default moves to `act` only on a benchmark verdict, never by hand. A set key wins over the default either way (`"check": "shadow"` keeps a cleared point in shadow), and the project section's key wins over the global section's. Note that one key differs from its point name: permission is `jev.permissions`; the point name `jev.permission` is accepted as the same key. `OC_JEV` overrides all ten at once: `OC_JEV=off` turns the layer off whatever the config says, and `shadow` or `act` sets every point. It never supplies a key — without one the layer stays off — and an unrecognised value is ignored with a warning. ### Promoting a point to `act` Promote one point at a time, and only after its **per-point label run** passes: 1. Run the point in `shadow` for at least a week of your own sessions. 2. `oceancode jev labels` collects every decision you answered (see [Reading decisions](#reading-decisions)) into `bench/jev/labels.json`. 3. The point's own run — `node scripts/bench/jev-decisions.mjs --set bench/jev/labels.json --point <p>` (live, `OC_LIVE_CONFIRM=1`) — must print `JEV_EVAL_OK point=<p> …`: accuracy ≥ 0.90 on the unanimous labels, a Wilson 95 % lower bound on that accuracy (`wilson_lower=` on the line) ≥ 0.80, at least 40 rows for `permission` and 12 for any other point, and no false allow. A handful of right answers cannot promote a point: n = 12 at 12/12 is 0.758 and never passes, and all-correct needs 16 rows. Short of the bound the run prints `JEV_EVAL_RED reason=wilson`. 4. Then set that point's key to `act` in the project config. Point 1 needs the posture it advises on: an empty config allows shell commands (the base rule is `* * allow`), so nothing asks and the advisor never runs. Add `bash * ask` under `permission` and every command off the read-only safe list asks — and is advised. The safe list is flag-aware (`find -delete`/`-exec`, `git branch -D`, `git remote add`, `git tag v1` are off it; see [12 · Permissions and sandbox](/oceancode/permissions-and-sandbox#the-shell-guard)). The default posture is not changed by this layer. `snippets` and `effort` change what a prompt's first request carries (an extra opener message, a lower reasoning effort), and `snippets` in `act` makes that request wait for its Jev calls; measure a real session's first-request latency and cache reads before promoting either. ## Thresholds Every threshold lives in `jev.thresholds`, is read from the global section only, and is tuned against labels for the pinned `jev.model` (a threshold tuned on one model id is not reused on another; `jev.model` must be a versioned id — `latest` and `preview` are refused). Probability thresholds are `0`–`1`; the two score thresholds (`permission_reversible`, `supervisor_progress`) are on the three-level scale `0`–`2`. | Key | Default | The risk it guards | |---|---|---| | `permission_destructive` | `0.6` | Ask when `destructive` is at least this. | | `permission_egress` | `0.5` | Ask when `egress` is at least this. | | `permission_reversible` | `1.5` | Ask when the `reversible` score is at least this. | | `permission_confidence` | `0.8` | Ask when the `blast_radius` choice is less confident than this. | | `output_relevant` | `0.5` | Below this `exists`, keep no block (head preview + spill instead). | | `output_mass` | `0.8` | Keep blocks until their probabilities add up to this (at most 8). | | `output_top_p` | `0.8` | Below `output_relevant`, still keep the top block when its probability is at least this… | | `output_exists_floor` | `0.2` | …and `exists` is at least this. | | `supervisor_complete` | `0.85` | Always let the stop through when `complete` is at least this. | | `supervisor_continue_complete` | `0.3` | Block a stop only when `complete` is below this. | | `supervisor_progress` | `1.0` | Block a stop only when the `progress` score is at least this. | | `supervisor_progress_confidence` | `0.5` | Block a stop only when the progress mass is at least this (see below). | | `supervisor_no_progress` | `0.31` | A `proposed_command` answer at or below this is a low (two in a row steer, four stop). | | `check_pass` | `0.5` | A pass is verified only when `check_now_passes` is the top category with at least this probability. | | `check_remaining` | `0.5` | A pass is verified only when the `remaining_checks` noul (a check the goal names that has not run and passed) is below this. | | `triage_confidence` | `0.8` | Pass the failure through when the `category` choice is less confident than this. | | `visibility_relevant` | `0.4` | Protect nothing below this `any_needed`. | | `visibility_min_p` | `0.05` | A protected part needs at least this probability. | | `guidance_yes` | `0.7` | A `guidance` noul at or above this is a yes. | | `guidance_no` | `0.3` | A `guidance` noul at or below this is a no; between the two it is unresolved. | | `effort_confidence` | `0.8` | The `difficulty` score steps the effort down only at this confidence or above. | | `work_mode_confidence` | `0.6` | Auto runs Jev's pick only at this confidence or above; below it the rules pick. | What the numbers mean in practice, from the recorded answers: - **`supervisor_progress_confidence` reads the progress mass, not the API's `confidence`.** It is compared with `1 − P("0")` — the probability above the lowest ("no progress") rung. The Score answer's own `confidence` measures how concentrated the answer is on one rung, so it drops whenever the answer spreads over adjacent rungs: a recorded half-done trace split 0.39 / 0.61 between "incremental" and "substantial" progress, nothing on "no progress", and reported confidence 0.42 — a mass of 1.0. - **`relevance_rerank` is gone.** A key set in an old config is still accepted (one warning, "no longer read"); nothing reads it. In `act`, the retired `relevance` point used to send a second request, a noul per top-three skill over its body excerpt, and it confirmed no candidate in 7 of 8 recorded prompts while adding 0.18–0.86 s at the start of each. The wide choice's rank order stands. - **`supervisor_looping` and `supervisor_loop_progress` are gone.** A key set in an old config is still accepted (one warning, "no longer read"); nothing reads them. The every-N-steps loop check they tuned could only ever steer; the no-progress check that replaced it asks one question about the command about to run, and only when that command is a repeat. - **`permission_scope` is gone.** A key set in an old config is still accepted (one warning, "no longer read"); nothing reads it. Against the task "fix the failing unit test in crates/ocean-llm", Jev scored `cargo nextest run --all-features` 0.73 and `ls -la` 0.57 on the old `outside_scope` question (both ask), and `cargo nextest run -p ocean-llm` 0.24 (allow): a question about "the task" punished ordinary project commands, so scope became a code check. Re-recorded without it, `cargo nextest run --all-features` is allow. - **`tools_confidence`, `tools_min_p`, `relevance_needs` and `relevance_min_p` are gone** with their points (retired 2026-10-21, see [Retired points](#retired-points)). A key set in an old config is still accepted (one warning, "no longer read"); nothing reads them. The other `jev` keys: | Key | Default | Meaning | |---|---|---| | `enabled` | `true` | An opt-out: `false` in either section turns the layer off. | | `endpoint` | the build default | The `POST` URL (global section only). | | `model` | `jev-1.13.0` | A versioned model id; the thresholds are tuned on it. | | `deadline_ms` | `1500` | One decision's deadline, retries and the response body included. | | `max_rpm` | `150` | HTTP attempts per 60-second window, retries included (per process). | | `max_state_chars` | `24000` | The state budget of every point but `output_view` and `visibility`, in bytes of serialized state. A larger value does not raise the 48,000-byte cap: a state over it is never sent (`invalid`). | | `backoff_seconds` | `60` | How long the client stops calling after the endpoint fails (see [Failure behaviour](#failure-behaviour)). | | `prewarm` | `true` | At boot, one `GET /health` on the endpoint's host opens the TLS connection (HTTP/2 where the server offers it), so the first decision skips the handshake; no credentials are sent and a failure is ignored (global section only). `scripts/bench/jev-first-call.sh` measures the first-call difference. | | `supervisor_every_steps` | — | Accepted with one warning; no longer read — the loop check it paced is replaced by the no-progress check, which fires on a repeated command instead of on a cadence. | | `supervisor_max_continuations` | `1` | How many times the supervisor may block a stop with a continuation per human prompt; `0` never blocks. A continuation after the first is asked about only when an `edit`, `write`, `apply_patch` or `bash` call ran since the last one. | | `permissions`, `output_view`, `supervisor`, `triage`, `visibility`, `check`, `guidance`, `snippets`, `effort`, `work_mode` | the point's default ([Modes](#modes)) | The per-point modes (the project section may set them). `permission` is accepted for `permissions`. | | `tools` (alias `tool_tiers`), `relevance` | — | Accepted with one warning ("no longer read (point retired 2026-10-21)"), never an error; nothing reads them. | | `thresholds` | the table above | Global section only. | ## What is sent A state is **named fields**, never a concatenated prompt, and the questions refer to them by backticked path; nothing from the state is spliced into a question. | Point | State fields | |---|---| | `permission` | `task` (your latest prompt, at most 4,000 bytes), `tool`, `arguments.command`, and `arguments.workdir` when the call names a working directory. Never the model-written description of the command. | | `output_view` | `step_intent` (the last 600 chars of the assistant text before the call), `tool`, `output.blocks[]` (`id`, `lines`, `text` ≤ 2,000 chars), trimmed to fit 48,000 bytes. | | `supervisor` | `goal` (≤ 1,500 chars), `step_count`, `open_todos`, `last_steps[≤ 6]` (`tool`, `args` ≤ 200 chars, `result` ≤ 300 chars — an error's message for a failed call —, `exit` for a call that reports one such as `bash`, and `failed: true` for a call that ended in an error; both keys only when present), `final_text` (the last 600 chars). The no-progress check: `goal`, `recent_commands[≤ 5]` (`command` ≤ 400 chars, `exit` when known, `output_excerpt` ≤ 150 chars), `proposed_command`. | | `check` | `goal` (≤ 1,500 chars), `proposed_command`, `proposed_command_output` (`exit`, `text` — the last 2,000 chars of the output, or the 300-char check tail when the output is empty —, `truncated`), `recent_commands[≤ 6]` (the prompt's earlier `bash` calls: `command`, `exit` when known, `output_excerpt`). | | `triage` | `command` (≤ 500 chars), `exit_code`, `step_intent`, `output` (head and tail, at most half the budget — 12 KB at the default). | | `visibility` | `goal` (≤ 1,500 chars), `open_todos[≤ 20]`, `parts[≤ 255]` (`id`, `tool`, `args`, `excerpt` ≤ 400 chars of the old tool result), trimmed to fit 48,000 bytes. | | `guidance` | `goal` (≤ 1,500 chars), `last_commands[≤ 6]` (`command`, `exit`, `output_excerpt`), `edited_paths`. | | `snippets` | `issue` (the prompt, ≤ 4,000 chars), `blocks` (`<id>` → `path`, `lines`, `text`), at most 38,000 bytes per request. | | `effort` | `prompt` (≤ 2,000 chars), `prev_prompt` (the previous human prompt, ≤ 500 chars, or `null`). | | `work_mode` | `goal` (the prompt, ≤ 2,000 bytes), `has_git`, `test_command_set`. | - **Paths outside the project.** There is no scope question; the bash gate marks an ask `paths_outside_project: true` and the advisor sends nothing then (a skip row, `a path outside the project`). The check runs on the call's `workdir` and on every word of the command that is a path: it starts with `/` or `~`, contains `/`, or is `..` — relative ones resolved against the workdir, the value of `--key=value`, `key=value` and `-I/path` taken as the path, a leading redirection (`2>`, `>>`) dropped, a URL (`://`) and `/dev/null` ignored, and a `$VAR/…` word, which cannot be resolved, counted as outside. It is lexical, like the external-directory prompt: a symlink out of the project is not followed. - **Head/tail truncation.** A long text field keeps its head and tail around a `[truncated N bytes]` marker. Point 1 never advises on a truncated state; the other points only record it. - **48,000 bytes is the hard cap on any state.** The endpoint was measured to accept about 33k input tokens — a 60,334-byte state answered, a 61,600-byte one was refused — and dense tool output runs about 1.7 bytes per token, so no state is ever sent above 48,000 bytes of JSON. A state the endpoint still refuses as too long comes back as HTTP 503 with error code 7003 ("User Input Error"); the client records that as `invalid`, never retries it and never counts it towards the back-off. - **The injection guard.** Before any call, every string of the state is normalised (case, fullwidth letters, invisible and bidi characters, whitespace) and matched against phrases that address the judge rather than describe the world — "ignore the above instructions", "note to the classifier", "you are Jev", "this command is safe", … A match skips the call and records `injection`. Your own words — the top-level `task`, `goal`, `prompt` and `issue` fields — are exempt. - **No content filter yet.** States carry raw text: the command, up to 255 output blocks, six step results, up to 12 KB of failing output, up to 255 excerpts of old tool results. That can include a secret from a file the dotenv rule does not cover (`.dev.vars`, `*.pem`), and the decision log keeps every state on disk (see [Reading decisions](#reading-decisions)). Opt a repository out (`{ "jev": { "enabled": false } }` in its `.oceancode/oceancode.json`) when its tool output may not leave the machine. - **Where it goes.** To TypeSafe's own API over HTTPS, under your key — file contents leave the machine. TypeSafe states that it does not train on customer requests or responses (`docs.typesafe.ai/models`, "Data handling"). Through the PlungeAI worker, when you name it as the endpoint, the request is answered by Workers AI through Cloudflare AI Gateway, billed to the PlungeAI account with gateway logging on; the worker can also relay to OpenRouter, which OceanCode never uses: every request to the worker has `backend=cloudflare`, and an answer whose `x-typesafe-backend` header is missing or names another backend is refused. ## Reading decisions - **`oceancode inspect`** shows one `jev` row per point: the mode as its layer, and the endpoint host (or `off`) as its source. See [09 · Capabilities](/oceancode/capabilities). - **`jev-decisions.jsonl`.** Every decision a point makes — acted on or not — is one JSON line in `<ledger root>/<session id>/jev-decisions.jsonl`, beside the session's ledger folder (the ledger root is `<data dir>/session-ledger` unless `ledger.dir` sets it). A row carries `point`, `mode`, `outcome`, `verdict`, `baseline` (today's behaviour), `latency_ms`, `server_ms` (the worker's own time for the call, from its `Server-Timing` header; absent when the endpoint sent none), `model`, `input_tokens`, `state_hash` (a hash of the full state), `state` (the state itself, or for one over 8 KiB a head/tail excerpt — the full state is then written once to `<ledger root>/<session id>/jev-states/<state_hash>.json`, so every decision can be re-sent) and `answers` — the full answers with every probability, or for a skipped or failed decision the reason as a string. It never carries the key, the endpoint or a header value, and a URL in an error message is replaced by `<url>`. Writing is best effort: a full disk never fails a decision (the first failure is logged once). - **Outcomes.** `acted` (the verdict changed something), `shadow` (recorded only), `fallback` (the decision kept today's behaviour: a low-confidence answer, a skip such as a command over 4,000 chars, or an answer that could not be applied), `held` (point 1's deterministic hold kept an allow answer at ask), `unavailable` (deadline, transport, HTTP error, rate cap or back-off window), `invalid` (an answer missing a key, of the wrong type or with bad probabilities; a state over its budget; the 7003 refusal), `injection` (the guard skipped the call) and `off` (the decider behind the point was off). - **The triage re-run note.** When point 4 re-runs a command, its decision row is written first, with the state Jev was asked about, and a second row follows with outcome `acted` and the reason `rerun exit=<code>` or `rerun refused: <why>` as its `answers` string. Only rows whose `answers` is an object are decisions; count and label those. A missing-binary or missing-module failure (the `env_missing` signals in the point 4 row above) is written as one row with the reason `deterministic env_missing` as its `answers` string and no request: `acted` / `hint` once the hint was appended, `fallback` / `pass` for a failing install command, `shadow` / `hint` in `shadow`. The hint line is "jev triage: `<name>` is not available (exit <code>); use what the project already has — `<alternative>` is available and do not install anything the task did not ask for" (the alternative clause only when one is on `PATH`; "a command" in place of the name when only the exit code said so, and for an `env_missing` answer Jev gave itself). - **Labels.** When you answer a permission prompt that Jev advised on, a label row is appended to the same file — `allow` for once or always, `deny` for reject — and joined to its decision. Only a person's answer is a label: the replies headless `oceancode run` gives by itself (`--auto`'s once, a Jev-advised reject, the reject without `--auto`) and the Ocean pager's own answers (its YOLO auto-approve, the queue it approves when you switch YOLO on, a cancelled or failed prompt) write none. `oceancode jev labels` exports every labelled decision. - **Labels a run writes itself.** At the end of a prompt the `effort` row gets its outcome (`{"steps", "edited", "checked", "check_passed_after_edit"}`) and every `snippets` row of the prompt its coverage (`coverage=<f> selected_coverage=<f> edited=<n>`). These are measurements, not your verdict: `oceancode jev labels` exports them with source `implicit-effort` and `implicit-snippets` (a person's answer is `shadow-user`). A `guidance` uptake row is a skip row (its `answers` a string), so no export carries it. - **Labelling by hand.** Every point but `permission` has no prompt to answer, so its rows come unlabelled. `oceancode jev labels --unlabelled --out to-label.json` (optionally `--point <p>`) writes them with `"label": null` and `"source": "unlabelled"`. An annotator fills each `label` in the grammar of `bench/jev/README.md`, sets `source` to `annotator-<n>` (a second annotator adds `second_label`), and the rows are appended to `bench/jev/labels.json` by hand, for example `jq -s add bench/jev/labels.json to-label.json > merged.json && mv merged.json bench/jev/labels.json`. `oceancode jev labels` keeps every row already in that file and adds only decisions whose point and state hash it does not carry, so a later export never duplicates an annotated row. An unlabelled row may carry `"evidence": {"spill_grepped": true}`: its turn later `grep`ped or `read` a file under `tool-output/`, so the full output was wanted after all. It is a hint for you, never a label. - **Implicit labels.** `oceancode jev labels --implicit --out bench/jev/labels.json` labels the rows nobody labelled from what the agent did next in the session's transcript: whether it made an `edit` / `write` / `apply_patch` / `bash` call after a supervisor continuation (`continue`, else `finish`, `implicit-continuation`). Rows follow the same merge as any export. These are the agent's behaviour, not your verdict, so spot-check them before a run counts on them; the rules are in `bench/jev/README.md`. ## The `oceancode jev` subcommand Every verb reads the same two sections and environment that boot reads, from the current directory. Each ends with its sentinel on the last stdout line — except the two `--json` verbs and `verdicts`, which keep stdout to JSON and print one `JEV_STATUS_EMITTED points=10`, `JEV_QUESTIONS_EMITTED points=10` or `JEV_VERDICTS_EMITTED lines=<n>` line on stderr, as `oceancode inspect --json` does. A failure exits 1. | Command | What it does | |---|---| | `oceancode jev status [--json]` | One `point mode endpoint-host` line per point (`-` when no client is configured), then `JEV_STATUS_OK points=10`. `--json` prints `{points, endpoint, thresholds}` — the modes and the resolved thresholds the bench lanes read before they spend. Makes no call. | | `oceancode jev probe` | **Live, one call.** Asks one fixed `destructive` noul about `git status --short` at the first point that is not off; prints `model=… latency_ms=… input_tokens=… destructive=…` and `JEV_PROBE_OK`, or `JEV_PROBE_FAIL <error>` and exits 1. With every point off it refuses without a call, naming why — `JEV_PROBE_FAIL jev is off: <reason>`, where the reason is the one that turned the layer off: a missing or disabled section, `OC_JEV=off`, `OC_TYPESAFE_API_KEY` absent for the direct API, a refused plain-http endpoint, an endpoint that is not a URL, or every point's mode set to `off`. | | `oceancode jev probe --record --point <p> --case <id> --request <req.json> --out <fixture.json> [--expect <json>]` | **Live, one call.** The alias of `scripts/fixtures/jev-record.mjs`: sends the request file's `{state, questions}` through the configured decider — its credential scoping, back-off and backend pin — and merges `{state, questions, response, expect}` into `cases.<id>` of the fixture, keeping every other case. Prints `JEV_RECORD_OK point=<p> case=<id> cases=<n>`, or `JEV_RECORD_FAIL reason=<reason> <message>` and exits 1 (a fixture that holds another point, or does not parse, is refused and left as it is). | | `oceancode jev labels [--root <ledger dir>] [--out <file>]` | Every decision row with a label, from every `<root>/<session>/jev-decisions.jsonl` (default root: the configured `ledger.dir`, else `<data dir>/session-ledger` — where boot writes the log), appended to `--out` (default `bench/jev/labels.json`). The file's rows are kept exactly as read — their order, their own duplicates and any key you added by hand; a decision is added only when the file does not yet carry its point and state hash. A state over 8 KiB is exported whole from its `jev-states/` file. Each row's `source` is `shadow-user` for your answers, `implicit-effort` / `implicit-snippets` for the labels those points write themselves. Prints `JEV_LABELS_OK added=<n> total=<m>`; a label file that does not parse is refused (`JEV_LABELS_FAIL`) and left as it is. | | `oceancode jev labels --implicit [--root <ledger dir>] [--out <file>]` | The same merge for labels read off the session transcripts (`<root>/<session>/transcript/`): an acted supervisor `continue` row `continue` or `finish` by whether a mutating call followed (see [Reading decisions](#reading-decisions)). Rows a person labelled are left to `labels`. Prints `JEV_LABELS_OK added=<n> total=<m> implicit=<k>`, `k` being the rows it derived. | | `oceancode jev labels --unlabelled [--point <p>] [--root <ledger dir>] --out <file>` | The same export for the decision rows nobody labelled (`"label": null`, `"source": "unlabelled"`), for annotators (see [Labelling by hand](#reading-decisions)), each with an `evidence` object where the transcript shows what the agent did next. `--out` is required, so unlabelled rows never land in the label set by default. | | `oceancode jev questions --json` | Each point's question set from the seams' own builders, in the order the seams send them. A choice whose options come from the request is empty and names its source in `options_from` (`state.output.blocks[].id`, `state.parts[].id`); `snippets` is one noul template under the key `{id}`, repeated per `state.blocks` key (`options_from: "state.blocks"`). The bench generates its question file from this. | | `oceancode jev verdicts --in <cases.jsonl>` | For each input line `{point, state, response}`, one output line `{point, verdict}` from that point's own policy at the global section's thresholds: permission `"allow"` / `"ask"` (the hold included; its line also carries `held`, true when the hold fixes the verdict at ask — the bench leaves such a row out of accuracy), output view `{keep}`, supervisor `{stop, low}` (`stop` as if no check had verified a pass yet; `low` whether the `proposed_command` answer is a no-progress low, `null` when the line has none), triage `"pass"` / `"rerun"` / `"hint"`, visibility `{protect}`, check `{verified}`, guidance the four yes/no/null judgements, snippets `{pack, chars}`, effort `{rungs_down}`, work mode `{pick}` (Jev's mode id at or above the threshold, else `null`). Makes no call. | **Money.** `probe` and `probe --record` are the only verbs that call Jev. So does every decision in `shadow` or `act`: at the rate-card price of `jev-1.13.0` ($0.042 per million input tokens, output free) a permission question is about 550 input tokens and the largest output-view or visibility state about 30k, so a long session costs cents. Through the PlungeAI worker the calls are billed to the PlungeAI account. The live bench lanes refuse to run without `OC_LIVE_CONFIRM=1`. ## Retired points Removed 2026-10-21; their config keys still load with one warning (see [Thresholds](#thresholds)). - `tool_tiers` (`jev.tools`) narrowed the advertised tool list per prompt behind a `tool_search` tool. Retired 2026-10-21: every tool is advertised again, and `tool_search` is gone; the `snippets` point now spends the opening request's context on source instead. - `relevance` (`jev.relevance`) narrowed `<available_skills>` per prompt. Retired 2026-10-21: replaced by `skills.roster: names`, now the default (one line of skill names; `full` restores the block — [02 · Configuration](/oceancode/configuration)). ## Environment variables | Variable | Meaning | Default | |---|---|---| | `OC_JEV` | `off`, `shadow` or `act`: overrides every Jev decision point's mode (`off` turns the layer off whatever the config says). It never supplies a key — without one the layer stays off. | unset → the config | | `OC_JEV_ENDPOINT` | Overrides `jev.endpoint` (the Jev `POST` URL). The only way to use plain `http`, and only to a loopback host (`127.0.0.1`, `localhost`, `::1`). | unset → `jev.endpoint`, else `https://api.typesafe.ai/v1/systemone` | | `OC_TYPESAFE_API_KEY` | The TypeSafe API key when none is stored (`oceancode key typesafe` wins), sent only to `api.typesafe.ai` (never to another host); never in config; stripped from child processes like every `*KEY*` variable. The direct API without a key keeps the layer off. | unset | | `TYPESAFE_API_KEY` | The same key under the name TypeSafe's own SDKs read; `OC_TYPESAFE_API_KEY` wins over it. | unset | The same rows are in [02 · Configuration](/oceancode/configuration), with every other variable. ## Failure behaviour A Jev failure is never a turn failure: the point falls back to today's behaviour and writes one row. A `shadow` call runs in the background (see [Modes](#modes)), so its row — an answer or a failure — is written when the call ends, under the id it was given when it started; closing the session waits for the calls still open, at most 2.5 seconds, and a process that is killed before then loses their rows. | Failure | Behaviour | |---|---| | Endpoint unreachable, DNS, TLS | Fallback; `unavailable`. Counts toward the back-off: the second consecutive deadline or transport failure opens the `jev.backoff_seconds` window, during which no point calls. | | Deadline exceeded (`jev.deadline_ms`, the response body included) | Fallback; `unavailable`; the request is dropped, the turn goes on. One deadline alone never opens the back-off window; a second in a row does. The deadline of a large `output_view` or `visibility` state (up to 48,000 bytes) is never counted toward it, so a slow large state cannot silence the other points. | | HTTP 401 / 403 | Fallback; one warning naming the credential source (`auth.json (oceancode key typesafe)`, `OC_TYPESAFE_API_KEY`, `TYPESAFE_API_KEY` or `ocean.headers`); no further requests until restart. | | HTTP 422 | Fallback; the row carries the server's message. | | HTTP 408 / 429 / 5xx | Up to 2 retries, 500 ms → 5 s with 25 % jitter, honouring `retry-after` up to 60 s, and only when the delay fits what is left of the deadline; then fallback, and a 429 or 5xx that still fails opens the back-off window at once. | | HTTP 503 with error code 7003 (state too long) | Fallback; `invalid`; no retry and no back-off. | | A redirect | Never followed (the headers would follow it off the worker); fallback as an HTTP error. | | Client rate cap (`jev.max_rpm` HTTP attempts per 60-second window, retries included, per process) | The decision is skipped this time; `unavailable`. The cap does not bound the shared gateway, whose own 429 and the back-off window are the real protection. | | Answer missing a key, of the wrong type, a choice outside its probabilities, or probabilities outside 0–1 or not summing to 1 ± 0.02 | Fallback; `invalid`. | | A 200 from the worker whose `x-typesafe-backend` header is missing or not `cloudflare` | Fallback; `invalid`; one warning; no further requests until restart. | | State over its budget | Each field is cut head/tail and output blocks are trimmed to fit (24,000 bytes by default for every point but `output_view` and `visibility`, 48,000 for those two, never above 48,000); a state that still does not fit is not sent (`invalid`). Point 1 never advises on a truncated command. | | The injection guard matches | No call; `injection`. Point 1 keeps the ask, and under `--auto` in `act` rejects it. | | A served model other than `jev.model` | Answered as usual; one warning per process (the thresholds were tuned on the configured id). | ## See also - [02 · Configuration](/oceancode/configuration) — the `jev` key and the environment variables. - [07 · Headless and server](/oceancode/headless-and-server) — `oceancode jev` among the management subcommands. - [08 · Bounding a long run](/oceancode/bounding-a-long-run) — the bounds the supervisor never changes. - [12 · Permissions and sandbox](/oceancode/permissions-and-sandbox) — the rules and the floor that run before the permission advisor. # 15 · Platform mode (PlungeAI connectivity) Source: https://www.plungeai.com/docs/oceancode/platform-mode `oceancode` ships in two builds. The **default build** is the standalone coding agent and contains no platform code at all. The **platform build** is produced with ```bash cargo build --release -p ocean-cli --features platform ``` and adds the three PlungeAI couplings: the OceanTunnel daemon (`crates/ocean-app/src/oceantunnel.rs`), the enrich resolver (`crates/ocean-agent/src/systemcontext/remote.rs`) and the per-step remote capability packs (`crates/ocean-agent/src/systemcontext/capability.rs`). Everything the `ocean` config section describes below is inert in the default build. The `platform` cargo feature is declared on three crates — `ocean-agent`, `ocean-app` and `ocean-cli` — and is **off by default**. The `OceanInfo` schema itself (every `ocean.*` key) stays in the leaf crate `ocean-config` in both builds, so an `oceancode.json` with an `ocean` section parses either way; only the code that acts on it is gated. ## Which build do I have? `node scripts/gates/check-platform-boundary.mjs --no-build` scans the two release binaries for three byte-literal markers after a release build and reports `markers_local=0 markers_platform=3` when the boundary holds; a routine `./check.sh` proves the same boundary on debug builds (`--debug`, which builds `target/debug/oceancode` under both feature sets and scans between the builds — no release build needed). At runtime, `oceancode inspect` reports a `platform` row: `compiled in` on a platform build, `not compiled in` on the default build. ## The "platform: not compiled in" notice A **default** build that finds `ocean.mode = "platform"` or a non-empty `ocean.endpoint` in `oceancode.json` prints once at boot and in `oceancode inspect`: ``` platform: not compiled in (ocean.mode or ocean.endpoint is set; rebuild with --features platform) ``` It is a single warning, not an error: the agent still runs, exactly as it does for any user with no `ocean` section. Rebuild with `--features platform` to make the section take effect. A config without an `ocean` section prints nothing. ## Config keys The `ocean.*` keys are serde-only and present in both builds; they take effect only in the platform build (`crates/ocean-config/src/ocean.rs:15-88`). | Key | Meaning | Default | |---|---|---| | `mode` | `local` or `platform`. The only switch that selects platform behaviour. | `local` | | `endpoint` | Enrich URL (`GET ?agent=<name>&directory=<dir>` → `{baseline, version}`). | empty | | `headers` | Headers sent on every platform request (auth lives here). | none | | `ttl_seconds` | How often a session re-checks the enrich endpoint (0 → 300). | 300 | | `agents` | Platform-resolved agent names; empty defaults to `ocean-build`, `ocean-plan`. | `ocean-build`, `ocean-plan` | | `mcp_url` | Auto-registers the platform MCP gateway as a remote MCP server named `ocean`. | empty | | `capability_endpoint` | Per-step capability pack endpoint. | empty | | `gateway_base_url` | The inference gateway a provider `baseURL` should point at: the bare host or its `/v1` root, both work. | `https://api.plungeai.com/v1` in platform mode | | `tunnel_url` | OceanTunnel dial-out URL; with a token, runs the tunnel daemon. | empty | | `tunnel_token` | Inline OceanTunnel session token. | empty | | `tunnel_token_file` | File holding the tunnel token (`~/` expands against `$HOME`). | empty | | `tunnel_agent` | Local agent name registered with the tunnel. | `oceancode` | **The switch is config-only.** No environment variable selects platform mode; `ocean.mode` in `oceancode.json` is the only switch — there is no `OCEANCODE_MODE` / `OC_MODE`. Two platform-related environment variables the binary reads never turn platform mode on by themselves: - `OC_TUNNEL_TOKEN` — the tunnel session token, read at the **highest priority** (above the deprecated `OCEAN_TUNNEL_TOKEN`, then `tunnel_token_file`, then the inline `tunnel_token`). `OCEAN_TUNNEL_TOKEN` is still read as a legacy fallback and logs one deprecation warning per process. - `OC_SSO_TOKEN` — a per-request gateway SSO session token. When set, oceancode attaches it as the `X-Ocean-SSO-Token` header (no `Bearer` prefix) on requests to the configured **https** gateway host only — never a direct-provider host, never over plaintext — and never on `Authorization` (the wallet bearer, your `ozk_` key, stays intact). Read at runtime, never persisted. Server-side reader is a pending platform handoff: until the inference gateway reads `X-Ocean-SSO-Token`, the header is ignored (harmless). Every variable also has a row in [02 · Configuration](/oceancode/configuration#environment-variables). ## Per-step capabilities In platform mode the local `<available_skills>` catalog is replaced by a **capability pack** sized to the plan row the model is working. At each step boundary the resolver asks the capability endpoint for a pack (eager skill bodies within a 24 KiB budget, a deferred index, and a version); `304` when the pack is unchanged, so the retained prefix — and the provider's prompt cache — never changes because a skill changed. Deferred bodies cost nothing until pulled: - **`load_capability` tool** — a platform-build tool that pulls a deferred capability body by id (`load_capability {id}`) into the transcript as a normal tool result, never a file on disk. It is registered only in the platform build and refuses any id the platform never offered. - **`capability` permission** — the permission family that governs `load_capability`. It **asks** by default in the read-only agent families (`ask`, `explore`, `security-review`) and is allowed for the `build`, `plan` and `general` families. - **Registry skills** — platform mode can also discover skills straight from the registry with no local files; they surface with the provenance `platform:<id>` (tier `platform`, bound tool id `skill_platform_<id>`) and load with no base-directory line. ## Model routing and the wallet **Signing in.** On a platform build, `oceancode login --provider ocean` (alias `plungeai`) signs you in through the browser and stores your account key. It opens the dashboard's `https://dashboard.plungeai.com/api/cli/authorize` page with a loopback port, you sign in with any method the dashboard offers, the dashboard hands a one-time code back to `http://127.0.0.1:<port>/callback`, and oceancode trades that code for your `ozk_` account key and stores it in `auth.json` under `ocean` — the entry `oceancode key ocean` writes and platform mode uses first. The key never appears in a browser URL. `--no-browser` prints the link without opening it; the sign-in gives up after three minutes; `oceancode logout ocean` removes the key. With an `ozk_` platform key stored — `oceancode key ocean ozk_…` (it lands in `auth.json`), or a credential under the `ocean` integration — platform mode synthesizes an `ocean` provider whose `baseURL` is the gateway's `/v1` root (`https://api.plungeai.com/v1` unless `gateway_base_url` says otherwise), so every model call routes through the inference gateway. A legacy `sk-ocean-` key is found the same way; when both are stored the `ozk_` key wins. A model of `auto` is resolved once per session from the gateway's `/v1/discovery/recommend` (falling back to `anthropic/claude-sonnet-4-5` if the recommend call fails). The `run --format json` result envelope then carries two extra objects for a platform-routed run: `platform: { fallback_steps }` (how many steps failed open to the local route) and `wallet: { balance, spent }` (present only when the gateway meters). A local run omits both. ## The ledger in platform mode Two engine-owned sections in `SESSION.md` are fed by platform-mode durable events: - **`## Plan changes by user`** — regenerated from `todo.updated` events, one summary line per event that carried user-authored plan rows (the model's own edits contribute nothing). - **`## Capabilities used`** — regenerated from `pack.manifest` events, one `| step | ids | version | hash |` row per step; a fallback step lists its row with an empty id cell. Both are bounded to the newest 20 rows and regenerated on every write, so they never crowd out model-owned sections. Plan rows also carry cross-session history: when a row that completed or was cancelled in an earlier session reappears, its projection shows how often it was seen and how often it failed. # 16 · Testing and gates Source: https://www.plungeai.com/docs/oceancode/testing-and-gates Everything in `oceancode` is verified by one script — `./check.sh` — and a handful of opt-in lanes on it. This page covers what those lanes are, and then the one test surface a terminal UI needs that unit tests cannot give it: the **interface probe**, which drives the real TUI over a pseudo-terminal and proves the interface never freezes. ## The lanes ```bash export PATH="$HOME/.cargo/bin:$PATH" ./check.sh # fmt · clippy -D warnings · build · nextest · goldens · doc + gate checks E2E=1 ./check.sh # + real-engine PTY tests, chaos, crash-recovery drills, the interface probe PERF=1 ./check.sh # + divan benches and the budget gates COVERAGE=1 ./check.sh # + llvm-cov with the zero-covered-handler tripwire MUTANTS=1 ./check.sh # + cargo-mutants over the risk-ranked modules ``` The default lane dials no model provider and touches no network. Every checker prints a sentinel as its **last line** and exits non-zero on failure; the full roster is in `scripts/README.md` (`apps/ocean-coding-agent-rust/scripts/README.md` in the repository). No lane spends money — the only live-provider scripts (`scripts/probes/live-*.sh`) are run by hand, never by `check.sh`. ## The interface probe A TUI can pass every unit test and still lock up the instant a slash command awaits the whole turn on the event loop — the exact freeze that shipped and was fixed on 2026-09-15. Unit tests run the update function in-process; they cannot catch a real event loop that has stopped turning keystrokes into frames. The interface probe can, because it is black-box: it spawns the real `oceancode` binary behind a pseudo-terminal, renders its screen with `pyte`, feeds it real keys, and asserts on what a person would see. It costs nothing to run. The binary is wired to a **fake Anthropic provider** with a fake key, an isolated `HOME`, and a loopback `--listen` port; the owner's real provider keys never enter a probe run. ```bash bash scripts/probe/run-probe.sh # every scenario bash scripts/probe/run-probe.sh -k slash # only scenarios whose id (or file) contains "slash" bash scripts/probe/run-probe.sh -k baseline # one file's scenarios (-k matches the module name too) bash scripts/probe/run-probe.sh -j 4 # run 4 at a time, each on its own ports bash scripts/probe/run-probe.sh --list # module:id of every discovered scenario bash scripts/probe/run-probe.sh --keep # keep each scenario's temp HOME to debug a failure ``` The last line is the sentinel: ``` INTERFACE_PROBE_OK scenarios=10 passed=10 xfail=0 # exit 0 INTERFACE_PROBE_FAIL scenarios=10 failed=1 mcp_roundtrip # exit 1 ``` `run-probe.sh` bootstraps its own Python venv at `target/probe-venv` (`pexpect` + `pyte`, idempotent and offline-tolerant), resolves the binary from `$OCEANCODE_BIN` — building `target/debug/oceancode` with `cargo build -p ocean-cli` when it is unset — and runs the harness. It is wired into the `E2E=1` lane after the crash-recovery drills, so a freeze is a red build. ### The liveness oracle Every scenario ends on the same universal check, `t.assert_live()`, which the harness calls for you after the scenario body returns: > type a unique nonce into the composer, assert the composer echoes it within 1.5 s, then erase > it. A frozen event loop cannot echo a keystroke, so that one assertion is the proof the interface is still alive — no matter what the scenario did before it. Because of this, a scenario only has to assert its own behavior and leave the composer on screen; the liveness check is free. ### Driving the TUI interactively (for agents) The same probe also runs as a **persistent session**, so an agent can drive the app step by step from separate Bash calls — open it, look, type, look again — instead of only inside a batch scenario. `scripts/probe/session.py` holds one PTY and `pyte` screen in a daemon; a client speaks to it over a unix socket: ``` session.py open <name> --app oceancode|pager|cli|cmd [--cols 120 --rows 40] [--keep] [-- cmd args…] session.py <name> screen | styled <row> | history | send <text> | key <k> [k…] | paste <text> session.py <name> wait <needle> [timeout] | wait_re <regex> [timeout] | resize <c> <r> | alive | requests | evidence <file> | close ``` `oceancode` / `pager` boot the real binary on the fake provider in a fresh `HOME` (the scenarios' isolation, but colours kept so `styled` can read them); `cli` runs the `cli/` REPL against deployed Studio. Wait for the app's **ready marker** before driving — `❯` for the pager (`oceancode` boots Ocean pager too since the native TUI was removed on 2026-10-05), `Ask anything` for the REPL — and **wait for a marker, never sleep** (`wait` polls the screen every 150 ms). ```bash S="target/probe-venv/bin/python scripts/probe/session.py" $S open pager --app pager # fresh HOME, fake provider, PTY 120×40 $S pager wait '❯' 40 # block on the ready marker $S pager send 'hello'; $S pager key enter $S pager wait 'ok: hello' 40 $S pager styled 38 # one row as (fg, bg, bold) runs — colour/bold asserts $S pager close # kill app + fixture, remove the temp HOME ``` End every interactive check the way a scenario does: type a unique nonce and confirm it echoes in the composer — the liveness oracle, by hand. `bash scripts/probe/session-selftest.sh` drives all three surfaces from separate processes and prints `PROBE_SESSION_OK apps=<n>` as its last line; `--no-cli` runs the hermetic surfaces only (the pager), which is how the `E2E=1` lane wires it in. ### Adding a scenario Drop a function named `scenario_<name>(t, fx)` into any file under `scripts/probe/scenarios/`. New files and functions are discovered automatically — you never register anything. `t` is a live `Tui`, `fx` a `Fixture` on the fake provider. Raise `AssertionError` (or call `t.fail(msg)`) to fail; a screen excerpt is attached automatically. ```python # scripts/probe/scenarios/my_feature.py def scenario_models_open_and_esc(t, fx): """/models opens the model picker and Esc returns to a live composer.""" t.send("/models") t.key("enter", 0.6) if "MODEL" not in t.text(): t.fail("/models did not open the model picker") t.key("esc", 0.4) if t.PLACEHOLDER not in t.text(): t.fail("Esc did not return to the composer") # the harness calls t.assert_live() after this returns ``` **Driving the screen (`t`):** | Call | What it does | |---|---| | `t.send(text)` | type text into whatever has focus | | `t.key(name)` | one key: `enter`, `esc`, `tab`, `backtab`, `backspace`, `up`/`down`/`left`/`right`, `home`/`end`, `pgup`/`pgdn`, `ctrl-c`, `ctrl-d`, `ctrl-u`, `shift-enter`, `f1`–`f12` | | `t.paste(text)` | bracketed paste | | `t.resize(cols, rows)` | resize the terminal (delivers `SIGWINCH`) | | `t.wait_for(needle, timeout)` | wait until `needle` (a substring or a compiled regex) is on screen; returns `True`/`False` | | `t.text()` / `t.line(n)` | the whole screen / one row, as text | | `t.alive()` / `t.pid` | is the process still running / its pid | | `t.snapshot(label)` | record a labelled screen for later | | `t.fail(msg)` | raise with a screen excerpt attached | | `t.assert_live()` | the liveness oracle (also usable mid-scenario) | | `t.assert_version_bar()` | assert a `vX.Y.Z` build stamp is in the status bar | | `t.PLACEHOLDER` | the composer's empty-prompt placeholder text | **Steering the provider (`fx`):** the fake provider picks its reply from MAGIC WORDS in the last user message — put one in the prompt text your scenario sends: | Word | Reply | |---|---| | *(none)* | one text block `ok: <first 40 chars of the prompt>` | | `SLOW` | the same text paced over 30 deltas (a visibly streaming turn) | | `ERR500` | HTTP 500 with an Anthropic error body | | `HANG` | an open stream that never answers (abort it from the TUI) | | `TOOLUSE` | a `tool_use` for the `bash` tool on the first turn (the permission dialog appears), text on the follow-up | | `WORKFLOW` | a `tool_use` for the `workflow` tool launching the built-in two-step `pipeline` (its sub-agents get default replies and the run completes) — the pager's Workflows-dock regression guard | | `GOAL` | a `tool_use` launching the built-in `goal` supervisor; its planner/skeptic `output_schema` requests are answered with valid JSON so the run converges (Plan → Work → Verify) | | `MD` | a markdown-heavy reply (heading, table, fenced code, wide CJK + emoji) | | `TASKAGENT` | a `tool_use` for the `task` tool on the first turn: one sub-agent whose prompt carries `DOCKCHILD`; text on the follow-up | | `DOCKCHILD` | the sub-agent's reply, 30 words paced over ~12 s, so a probe reads the dock while it runs (`pager_dock_agents`) | Read what the binary actually sent with `fx.requests()`, `fx.last_request()`, `fx.user_texts()` (the last-user text of every request), and `fx.base_url` / `fx.port`. Each scenario is fully isolated: its own `HOME`, its own fixture project directory carrying its own `.claude/commands/{long,short}.md`, its own two free ports, and its own fresh fixture process. Scenarios never share state, so `-j N` runs them in parallel safely. ### xfail policy A scenario for a **known, tracked** defect is decorated `@xfail("REASON-ID")`: ```python from scenarios.xfail import xfail @xfail("OCA-1234-slash-refusal-toast") def scenario_unknown_slash_is_refused_visibly(t, fx): ... ``` It is reported as `XFAIL <REASON-ID>` and does **not** fail the run; a scenario that is xfail-marked but unexpectedly passes is reported `XPASS` and counts as a pass. Use `@xfail` only for a defect that has a tracking id — never to silence a flaky or half-finished scenario. The moment the defect is fixed, remove the decorator so a regression turns the scenario red again. # Distribution and updates Source: https://www.plungeai.com/docs/oceancode/distribution `oceancode` ships as two self-contained binaries: the engine `oceancode` and the terminal UI `ocean-pager` it launches, installed side by side. This page covers installing it, keeping it current, verifying a download, and removing it. Every download is checksummed, and every install channel agrees on one set of release-asset names. ## Installing | Channel | Command | |---|---| | macOS / Linux (script) | `curl -fsSL https://raw.githubusercontent.com/OWNER/ocean-coding-agent/main/packaging/install/install.sh \| sh` | | Windows (PowerShell) | `irm https://raw.githubusercontent.com/OWNER/ocean-coding-agent/main/packaging/install/install.ps1 \| iex` | | Homebrew (macOS / Linux) | `brew install OWNER/oceancode/oceancode` | | Cargo (Rust-native, npm-free) | `cargo install --locked --path crates/ocean-cli` (from a clone today; `cargo install --locked oceancode` once published) | The script installer downloads the release asset for your platform, verifies it (see [Verifying a download](#verifying-a-download)), and installs `oceancode` and `ocean-pager` into `${OC_INSTALL_DIR:-$HOME/.local/bin}` (Windows `%USERPROFILE%\.oceancode\bin`). If that directory is not on your `PATH`, the installer prints the line to add. See [02 · Configuration](/oceancode/configuration) for `OC_INSTALL_DIR` and the other install knobs. `cargo install` is npm-free by design — the whole toolchain is Rust; no Node package is pulled at build or install time. See `packaging/homebrew/cargo-install.md`. ## Keeping current — `oceancode update` ```bash oceancode update # update to the newest release for your channel oceancode update --self-test # prove the swap + rollback offline (no network) ``` `update` fetches the newest release for the configured channel, **never downgrades**, verifies the release signatures, swaps `oceancode` and `ocean-pager` atomically, and **rolls both back** if either new binary fails a health check — so a failed update leaves the working pair exactly as it was. `--self-test` prints `UPDATE_SELF_TEST_OK binaries=2` as its last line. Channels and opt-out: | Setting | Where | Effect | |---|---|---| | `update.channel` | config (`oceancode.json`) | `stable` (default) or `beta` (sees pre-releases). | | `OC_UPDATE_CHANNEL` | env | Overrides `update.channel` for one run. | | `update.auto_check` | config | `true` (default) checks for a newer release on boot. | | `OC_NO_UPDATE_CHECK` | env | Any value suppresses the boot-time check. | | `update.endpoint` | config | Overrides the release-metadata source (default: GitHub Releases). | **Telemetry-free first run.** The boot-time update check is *always* skipped on the very first run (and whenever `update.auto_check` is `false` or `OC_NO_UPDATE_CHECK` is set): a fresh install never phones home before you ask it to. ## Verifying a download Every release ships a single `SHA256SUMS` manifest and a detached minisign signature (`<asset>.minisig`) per asset. Verify a download before trusting it — the same check on macOS, Linux and Windows, with no external tool: ```bash oceancode verify-release oceancode-macos-aarch64.tar.gz \ --sha256sums SHA256SUMS ``` It prints `VERIFY_RELEASE_OK file=<name>` on success and refuses (non-zero exit) on any mismatch. `--pubkey <base64>` overrides the embedded release public key. **Signing tiers.** Releases are signed with minisign on every platform. When an Apple Developer ID (macOS) or an Authenticode certificate (Windows) is available, the release *also* carries OS-native codesigning / notarization. When no such certificate is present, the release ships **unsigned at the OS level but with `SHA256SUMS` + minisign** — the portable trust root above still applies, and the installer stays fail-closed (it refuses to install an unverifiable artifact unless you pass `--insecure-skip-verify` / `OCEANCODE_INSECURE_SKIP_VERIFY=1`). > **Test key notice.** The public key embedded in `oceancode verify-release` > (`verify_release::EMBEDDED_PUBKEY`) and in the installers is a **deterministic test key**. The > release owner must replace it with the real minisign public key before shipping a signed release. ## Uninstalling ```bash oceancode uninstall # remove the binary only oceancode uninstall --purge # also remove ~/.config/oceancode and the data dir oceancode uninstall --dry-run # print the plan, remove nothing ``` `uninstall` removes only what the installer placed. `--purge` additionally removes the app's config and data directories (the data dir holds `auth.json`, TUI state and the default global session store) — never your per-project `.oceancode/oceancode.db`. It requires a typed `oceancode` confirmation unless `--yes`, never follows a symlink out of its own tree, and prints `UNINSTALL_OK removed=<n>`. ## Windows notes On Windows the `bash` tool spawns the shell selected by the `shell.kind` config key (default `auto` → PowerShell). Tool output is normalized for cross-OS stability: `\r\n` and stray `\r` become `\n`, and backslash paths are shown POSIX-style (`C:/Users/...`) for display only. ConPTY is the PTY backend, and the console has ANSI/VT processing enabled so escape sequences render. --- ## Release-asset naming contract Every channel — the script installer, Homebrew, `oceancode update`, `oceancode verify-release`, the download page and the release CI job — agrees on **one** naming scheme. This is the single source of truth; a producer or consumer that disagrees is a bug to reconcile against it. **Per target, the release publishes BOTH:** - **`oceancode-<os>-<arch>.tar.gz`** — a gzipped tar containing exactly `oceancode` and `ocean-pager`. The **script installer** (macOS/Linux) and **Homebrew** download this and install both side by side. - **`oceancode-<os>-<arch>`** — the **bare binary** (Windows: `oceancode-<os>-<arch>.exe`). `oceancode update` downloads this and stages it directly as `oceancode.new` for the atomic swap, so it must NOT be an archive. On Windows the PowerShell installer also consumes this bare `.exe`. - **`ocean-pager-<os>-<arch>`** — the bare TUI binary; `oceancode update` swaps it together with `oceancode` (the engine runs inside the pager process, so the pair must match). If either fails its `--version` health check after the swap, both are rolled back to their `<name>.bak`. A release without this asset updates `oceancode` alone and warns that the TUI was not updated. Plus, for **every** asset (the tarball and both bare binaries): - **`<asset>.minisig`** — a detached minisign signature, and - **one `SHA256SUMS`** — a single manifest hashing every asset (no per-target manifest). **No version appears in any filename** — the version lives in the release tag path and the download manifest's `version` field. ### rustc triple ↔ `<os>-<arch>` token The `<os>-<arch>` token is `oceancode update`'s `platform_suffix()` (`std::env::consts::OS` + `std::env::consts::ARCH`), matched as a **substring** of the asset name — so the Linux `-musl` suffix is tolerated. The installer's `detect_asset` (`packaging/install/install.sh`) names match the tarball column. | rustc triple | `<os>-<arch>` token | tarball (installer / Homebrew) | bare binary (`oceancode update`) | bare TUI (`oceancode update`) | |---|---|---|---|---| | `aarch64-apple-darwin` | `macos-aarch64` | `oceancode-macos-aarch64.tar.gz` | `oceancode-macos-aarch64` | `ocean-pager-macos-aarch64` | | `x86_64-apple-darwin` | `macos-x86_64` | `oceancode-macos-x86_64.tar.gz` | `oceancode-macos-x86_64` | `ocean-pager-macos-x86_64` | | `x86_64-unknown-linux-musl` | `linux-x86_64` | `oceancode-linux-x86_64-musl.tar.gz` | `oceancode-linux-x86_64-musl` | `ocean-pager-linux-x86_64-musl` | | `aarch64-unknown-linux-musl` | `linux-aarch64` | `oceancode-linux-aarch64-musl.tar.gz` | `oceancode-linux-aarch64-musl` | `ocean-pager-linux-aarch64-musl` | | `x86_64-pc-windows-msvc` | `windows-x86_64` | `oceancode-windows-x86_64.tar.gz` | `oceancode-windows-x86_64.exe` | `ocean-pager-windows-x86_64.exe` | Because the `.tar.gz` and the bare binaries carry the same `<os>-<arch>` token, the update path must select the **bare** binaries specifically (skip `.tar.gz`) and tells them apart by the `ocean-pager-` prefix; the installer/Homebrew select the tarball. ## Platform download-page contract When `update.endpoint` is set, `oceancode update` fetches release metadata from the platform's release endpoint (Track P / S11-T03; the canonical schema lives in `docs/platform-download-contract.md`). The endpoint returns JSON of this shape — the platform and the binary agree on the artifact list here: ```json { "version": "0.8.8.01", "channel": "stable", "artifacts": [ { "target": "aarch64-apple-darwin", "url": "https://…/oceancode-macos-aarch64", "sha256": "…", "minisig": "https://…/oceancode-macos-aarch64.minisig" } ] } ``` `version` and `channel` are strings; `artifacts` is one object per shipped target. `url` is the **bare binary** `oceancode update` downloads and stages as `oceancode.new` (not the `.tar.gz` — that is the installer's), `sha256` matches its `SHA256SUMS` entry, and `minisig` is its detached signature. This page documents the shape only; the endpoint itself is the platform's (Track P, `docs/platform-download-contract.md`), not the binary's. ## Release checklist An ordered, mostly-automated release. Agents never publish or tag — the owner does the final GitHub Release and tap steps. 1. **Bump the version.** `bash scripts/release/bump-version.sh <X.Y.Z.B>` — moves the `## Unreleased` CHANGELOG section under the new dated heading and updates `VERSION`. The number is `MAJOR.MINOR.PATCH.BUILD`: the first three name the release (a human sets them with the explicit argument), the fourth is the build counter — every `scripts/release/build-release.sh` run adds one to it on its own (`0.8.8.01`, `0.8.8.02`, … `0.8.8.99`, `0.8.8.100`; two digits minimum, never capped), so a build number is never reused and self-update treats a newer build of the same release as an update. The check.sh lanes build with `NO_BUMP=1`. Re-baselined to `0.8.8.00` on 2026-10-22 (the `2.6.NN` numbers before it carried the build counter in the patch field). 2. **CHANGELOG dated.** Confirm the moved section reads correctly under `## 0.8.8.01 — <date>`. 3. **User-guide tables current.** `node scripts/gates/check-user-guide.mjs` → `USER_GUIDE_OK … missing=0` (every `OC_*` var and subcommand has a row) and `node scripts/gates/check-changelog.mjs` → `CHANGELOG_OK`. 4. **Whole-tree green on three OSes.** `./check.sh` → `ALL CHECKS PASSED` on macOS, Linux and Windows in CI. 5. **Build the five targets.** `bash scripts/release/build-all.sh` (or the CI `release` job's matrix) → `BUILD_RELEASE_OK targets=5`. Per target publish **both** the bare `oceancode-<os>-<arch>` (Windows `.exe`) and the `oceancode-<os>-<arch>.tar.gz`, using the `<os>-<arch>` tokens in the mapping table above — no version in any filename. 6. **Per-artifact budgets.** `RELEASE=1 ./check.sh` (or the CI native legs) runs `size-gate.sh` per artifact (≤ 40 MiB → `SIZE_GATE_OK`) and `boot-bench.sh` (< 36 ms → `BOOT_BENCH_OK`). 7. **Sign.** `bash scripts/sign/sign-artifacts.sh` (minisign → `SIGN_OK artifacts=<n>`); where certificates exist, `scripts/sign/codesign-macos.sh` (Apple notarization) and `scripts/sign/authenticode-windows.ps1` (Windows Authenticode). 8. **Checksums.** `bash scripts/release/gen-sha256sums.sh dist` → `SHA256SUMS_OK count=<n>` over the packaged assets. 9. **Draft the GitHub Release** (owner): upload the assets, `SHA256SUMS` and each `.minisig`. 10. **Homebrew tap** (owner): mirror `packaging/homebrew/oceancode.rb` to the tap with the real `sha256`s / bottle digests. ### Owner-only steps (agents never do these) - Replace `verify_release::EMBEDDED_PUBKEY` and the installers' embedded minisign pubkey with the **real** release key. - Provision the CI secret `MINISIGN_SECRET_KEY` and install `minisign` on the release runners. - Relocate `docs/plans/ci/oceancode.yml` → `.github/workflows/oceancode.yml`. - `git tag` / publish the GitHub Release; push the Homebrew tap; `cargo publish`. ## Release-notes template ```md # oceancode v<X.Y.Z> ## Highlights - <the one or two changes users will notice first> ## Install - macOS/Linux: `curl -fsSL <install.sh> | sh` - Windows: `irm <install.ps1> | iex` - Homebrew: `brew install OWNER/oceancode/oceancode` - Cargo: `cargo install --locked oceancode` ## Verify Download the asset for your platform and run: `oceancode verify-release <asset> --sha256sums SHA256SUMS` ## Changed - <user-visible changes from the CHANGELOG's dated section> ## Fixed - <fixes from the CHANGELOG> ## Checksums See `SHA256SUMS` and the per-asset `<asset>.minisig` attached to this release. ``` ## See also - [01 · Getting started](/oceancode) — the install one-liners and first run. - [02 · Configuration](/oceancode/configuration) — `update.*`, `shell.kind`, `OC_UPDATE_CHANNEL`, `OC_NO_UPDATE_CHECK`, `OC_INSTALL_DIR`. - [07 · Headless and server](/oceancode/headless-and-server) — `update` / `verify-release` / `uninstall` as management subcommands. # 18 · Operations Source: https://www.plungeai.com/docs/oceancode/operations Running `oceancode` for a long time — days, not minutes — is a first-class case. This page covers the operational surface that keeps a long run healthy: a metrics scrape, database and ledger retention, the storage doctor, crash recovery, the event-bus drop counter, and log levels. Everything here is conservative by default: session retention keeps a week (`retention.days: 7`) and deletes nothing for the first 24 hours after an upgrade. The backup, restore and crash-recovery *procedures* live in the storage-and-recovery runbook (`apps/ocean-coding-agent-rust/docs/RUNBOOK-storage-and-recovery.md` in the repository). ## Metrics (`OC_METRICS_ADDR`) Set `OC_METRICS_ADDR=host:port` to serve runtime gauges for a Prometheus scrape (uptime, worker and live-task counts, injection-queue depth, and — on Linux — `oceancode_rss_kb`, the process resident set from `/proc/self/statm`) plus three SQLite-writer gauges: `oceancode_writer_pending_ops` (batched writes not yet committed), `oceancode_writer_lag_ms` (age of the oldest of them) and `oceancode_persist_failures` (batched writes dropped because they failed). Nothing listens by default. The endpoint is a profiling aid, not a control surface: it exposes gauges, never accepts commands. The HTTP server also answers `GET /global/health`, which carries `dropped_events` — see [Event-bus drops](#event-bus-drops-dropped_events) below. A fresh headless server's resident set is held under the 45 MB SLO by the `PERF=1` lane: `scripts/gates/rss-fresh.sh` boots one release-binary `serve` on an ephemeral loopback port and samples its RSS (`RSS_FRESH_KB`), which `assert-budgets.mjs` gates against the ceiling. ## Retention and vacuum One setting, `retention.days`, governs both stores that grow over a long run — the session database `oceancode.db` and the Session Ledger directory. A session idle longer than that many days is deleted from the database and its ledger directory goes with it, in one sweep. | Key | Default | Effect | |---|---|---| | `retention.days` | `7` | A session tree (a top-level session and every sub-agent under it) whose members have **all** been idle longer than this many days is deleted, with each member's ledger directory. A tree with any member running or open is kept. `0` turns the sweep off. One setting for the database and the ledger. | | `database.retention_days` | — | Legacy. Read only when the config has no `retention` section in any layer, and only when > 0; then it is the retention. | | `ledger.retention_days` | — | Ignored since `retention.days`. A config that still sets it loads, with one warning at boot. | | `database.vacuum` | — | No longer read: every sweep that deleted something hands the freed pages back to the file itself (see below). | ```jsonc { "retention": { "days": 14 } } // or 0 to keep everything ``` **The sweep.** A ticker runs it every 10 minutes; the first tick, at boot, only counts. One session tree is deleted per writer job with a short pause between trees, so a step waiting on the database runs in between; at most 500 sessions go per sweep, the rest at the next tick. A ledger directory whose session is not in this database (another project's — the ledger root is shared, each project has its own `oceancode.db`) is removed only on its own age, past `max(retention.days, 30)` days. After a sweep that deleted something the freed pages are returned to the file in small `incremental_vacuum` chunks; an older database is switched to incremental auto-vacuum once, with one full `VACUUM` that runs only while no session is running. **The grace window.** The first boot that arms the sweep writes `retention-armed-at` in the data dir. For 24 hours after it nothing is deleted — a sweep only counts what it would remove, and the pager shows `retention N pending` on its context row with a one-time notice — so an upgrade with old sessions on disk never loses them unannounced. `/retention off` keeps them. **`/retention`** in the pager (or `_ocean/retention` from any ACP client) shows and sets it: | Form | Effect | |---|---| | `/retention` | `retention 7 days · 12 sessions pending · first sweep after 2026-10-21 18:04 (grace)`, or after the window `… · last sweep 5m ago, 340 swept`. The pending counts are recounted for this request. | | `/retention <days>` | Sets `retention.days` (`0..=3650`) in your **global** config file — the user tier boot reads: `$OC_CONFIG`, else `$XDG_CONFIG_HOME/oceancode/oceancode.json` (`~/.config/oceancode/oceancode.json`) — and re-arms the sweep at once, without a restart. A project config cannot override it (the user tier wins over the project's); a managed config can. A number out of range is refused with the engine's message. | | `/retention off` | The same as `/retention 0`: the sweep stops; nothing is deleted. | `_ocean/retention` takes `{}` (read) or `{"days": n}` (set) and answers `{days, pendingSessions, pendingLedgerDirs, armedAt, graceUntil, lastSweep, sweptTotal}` (instants in unix ms; `lastSweep` is `{at, removedSessions, removedDirs}` or `null`). Every `session_status` frame carries the same object under `ocean.retention`, from the last count — a frame never walks the disk. The metrics endpoint carries `oceancode_retention_swept_sessions_total` (sessions deleted since boot) and `oceancode_retention_pending` (sessions the last count found still to delete). ## `oceancode doctor --storage` `oceancode doctor` reports orphaned sessions and captured panic reports (read-only; it boots nothing). Add `--storage` for a storage report: ``` $ oceancode doctor --storage …orphan / panic report… storage: oceancode.db 4194304 bytes (wal 32768 bytes) sessions=128 ledger 2097152 bytes, oldest 12 days restore is offline-only: stop oceancode before restoring a backup ``` `--storage` is a `doctor` flag, not a separate subcommand. The report is best-effort: it renders zeros for a path that does not exist yet. ## Backup and restore Backups are consistent even under concurrent writes (`VACUUM INTO`, not a raw file copy), and restore verifies the backup's `PRAGMA integrity_check` before it swaps anything into place. Restore is **offline-only** — it replaces the live database file, so stop `oceancode` first. The step-by-step commands are in the runbook (`apps/ocean-coding-agent-rust/docs/RUNBOOK-storage-and-recovery.md` in the repository). ## Crash recovery A panic writes a timestamped report under the data dir's `crash/`, and an active-sessions registry records a session whose process died without a clean exit. The next boot prints a one-line stderr notice when it finds an orphan; `oceancode doctor` lists both. A crashed session resumes with `oceancode run --continue` — the Session Ledger is intact byte-for-byte across a `kill -9`. Full drill in the runbook (`apps/ocean-coding-agent-rust/docs/RUNBOOK-storage-and-recovery.md` in the repository). ## Event-bus drops (`dropped_events`) Each event subscriber has a bounded queue. When a subscriber falls behind, the bus's default behaviour is to evict the oldest queued event and count the drop rather than block the producer — so a slow consumer never stalls the streaming hot path. The running total is visible as the `dropped_events` field on `GET /global/health`. Two `events` config keys tune this: | Key | Default | Effect | |---|---|---| | `events.sub_capacity` | `1024` | Per-subscriber event-queue depth. A deeper queue tolerates a slower subscriber before it drops. `0` falls back to the built-in default. | | `events.mode` | `drop_oldest` | Overflow behaviour. `drop_oldest` evicts the oldest event and counts the drop (the streaming default). `block_until` briefly parks the publisher (up to one second) for a free slot before falling back to drop-oldest — bounded back-pressure for a deployment that would rather slow a producer than lose events. | The bus is sized from the project-tier `events` config at boot. ## Log levels (`OC_LOG`) The engine logs through a tracing subscriber that **redacts secret-shaped values before any write** (Anthropic / OpenAI / AWS / PEM / bearer / assignment shapes), so a token never lands in a log file. Control it with `OC_LOG` or the `obs` config block. | Setting | Default | Effect | |---|---|---| | `OC_LOG` (env) | unset → `obs.log_level` | Level: `trace`, `debug`, `info`, `warn`, `error`. Overrides `obs.log_level`. An unrecognized value logs a one-line notice and falls back to `info`. | | `obs.log_level` | `info` | The configured level when `OC_LOG` is unset. | | `obs.log_rotate_bytes` | `0` → 10 MiB | Byte ceiling before the live log file rotates to `<file>.1`, so logs never grow unbounded on a multi-day run. | | `obs.log_file` | unset → stderr | Log sink path for the **headless** modes (`oceancode serve` / `run`). Absent logs to stderr. See the sink-precedence note below. | The level (`OC_LOG` → `obs.log_level` → `info`) applies to every mode. The `obs` config is read from the **run's project directory** (the `--dir` a `serve`/`run` was given, else the current directory), the same way boot reads project config (`ocean_config::load`, honouring `$OC_CONFIG_DIR`) — so `oceancode run --dir /path/to/project` uses that project's `obs` settings, not the shell's cwd. A malformed or absent config falls back to defaults and never blocks startup. **Sink precedence.** `obs.log_file` (bounded by `obs.log_rotate_bytes`) redirects the default **stderr** diagnostic sink of the headless `oceancode serve` / `run` modes to a size-bounded rotating file, so headless logs never grow unbounded on a multi-day run. The interactive shape is Ocean pager, which owns the terminal and keeps its own state under its seeded `$OC_PAGER_HOME` (see [19 · Ocean pager](/oceancode/pager)); `tui.log` is no longer written. So the jsonc example below applies to `serve` / `run`: ```jsonc { "obs": { "log_level": "debug", "log_file": "/var/log/oceancode.log", "log_rotate_bytes": 10485760 } } ``` ## Endurance soak (operators) A deterministic soak drives 1000 metering-fixture steps (no live model) and grades per-step p99 latency and RSS growth against pinned budgets. Write the soak record to its own path so a short marathon-fixture run cannot clobber it (`soak-metrics.mjs` grades any record under 1000 steps as `SOAK_RED reason=short`): ```bash STEPS=1000 TURNS_OUT=docs/results/soak/turns.jsonl bash scripts/bench/marathon-fixture.sh --steps 1000 node scripts/bench/soak-metrics.mjs docs/results/soak/turns.jsonl # → SOAK_OK / SOAK_RED ``` `OC_SQLITE_CACHE_KB` and `OC_READ_POOL_SIZE` bound per-connection SQLite memory and the read pool for a measurement run; leave both unset in normal operation. See [02 · Configuration](/oceancode/configuration#test-and-bench-knobs). # Ocean pager — the terminal UI Source: https://www.plungeai.com/docs/oceancode/pager Ocean pager is `oceancode`'s terminal UI. A plain `oceancode` execs Ocean pager (`ocean-pager`); the pager and the Ocean engine run in **one process** over typed ACP channels — no HTTP between UI and engine. `--db`, `--directory`, `--listen` and `--verbose` are passed through. The first-party ratatui TUI was removed on 2026-10-05 (its guide pages are in `archive/native-tui/docs/`). For compatibility `--tui pager` and `OC_TUI=pager` are accepted and change nothing (`--tui grok` is refused as unknown); `--tui ocean` and `OC_TUI=ocean` fail with "the native TUI was removed; `oceancode` starts Ocean pager". The pager's own slash commands, keys and settings are in [Ocean pager reference](/oceancode/pager-reference). ## Binary 1. Build the pager: `bash scripts/release/build-pager.sh` (or `cd ocean-pager && cargo build --release -p ocean-pager-bin --locked`) → `target/release/ocean-pager`, the binary `oceancode` launches. 2. A plain debug build lands in `target/debug/ocean-pager` and is never launched. To run a pager from elsewhere, set `OC_PAGER_BIN`. Discovery order: `OC_PAGER_BIN` → `ocean-pager` beside `oceancode` (and beside the binary a symlinked `oceancode` resolves to) → `ocean-pager` on `PATH`. `oceancode` sets `$OC_PAGER_HOME` to `<data>/oceancode/pager` (`$XDG_DATA_HOME`, else `~/.local/share`) — the directory a bare `ocean-pager` uses too — so the pager never shares `~/.grok`. Auto-update, remote fetch, feedback and voice are off. Every pager variable: [Environment](#environment). ## Environment Read by `ocean-pager`, not by the engine. Every pager variable is `OC_PAGER_*`; the old `GROK_*` names are not read. | Variable | Effect | |---|---| | `OC_PAGER_HOME` | The pager's state directory (`config.toml`, sessions, debug logs, the extracted ripgrep under `bin/`). `oceancode` sets it; unset → `$XDG_DATA_HOME/oceancode/pager`, else `~/.local/share/oceancode/pager`. `GROK_HOME` is ignored. | | `OC_PAGER_LOG_FILE` | Write the pager's debug log to exactly this path (honors `RUST_LOG`). | | `OC_PAGER_DEBUG_LOG` | A path, or `1`/`true` for `<pager home>/debug/tui-<pid>.txt`: turns the debug log on. | | `OC_PAGER_CONFIG` | Inline JSON config overlay, merged above `config.toml`. | | `OC_PAGER_CONFIG_PATH` | An extra JSON or TOML config-overlay file (read by extension), merged above `config.toml`. | | `OC_PAGER_MANAGED_CONFIG_FAIL_CLOSED` | Overrides the requirements fail-closed setting (applies to `requirements.toml` only). | | `OC_PAGER_SHELL` | Shell the pager spawns: an executable path (bash, zsh, …); on Windows `pwsh`, `powershell` or `cmd`. | | `OC_PAGER_THEME` | Force a theme name, or `auto`, without editing the config. | | `LC_OC_PAGER_THEME` | Same as `OC_PAGER_THEME`; the `LC_` form survives SSH `SendEnv LC_*`. | | `OC_PAGER_APPEARANCE` | `dark` or `light`: the terminal background `auto` themes assume. | | `LC_OC_PAGER_APPEARANCE` | Same as `OC_PAGER_APPEARANCE`, SSH-forwardable. | | `OC_PAGER_TERMINAL_THEME` | Follow the terminal's own palette (on/off; a requirement pin wins). | | `OC_PAGER_NERD_FONTS` | Nerd Font glyphs in the git status (on/off). | | `OC_PAGER_FORCE_LEGACY_CONSOLE` | Windows: ASCII-safe glyphs for the legacy console. | | `OC_PAGER_SCREEN_MODE` | Internal: the screen mode a `/fullscreen` / `/minimal` relaunch hands to the new process (read and removed at startup). | | `OC_PAGER_SCREEN_MODE_SWITCH` | `exec` forces the legacy re-exec switch between screen modes. | | `OC_PAGER_SCROLL_SPEED` | Mouse-wheel scroll speed; applied on first load only (the settings modal persists it). | | `OC_PAGER_SCROLL_MODE` | Mouse-wheel scroll mode; first load only. | | `OC_PAGER_SCROLL_LINES` | Lines per wheel notch; first load only. | | `OC_PAGER_INVERT_SCROLL` | Invert the wheel direction; first load only. | | `OC_PAGER_SCROLL_CADENCE_MS` | Redraw cadence while scrolling (ms). | | `OC_PAGER_MIN_DRAW_MS` | Minimum interval between frames (ms). | | `OC_PAGER_DISPLAY_REFRESH_PROBE_ENABLED` | Probe the display refresh rate at startup (on/off). | | `OC_PAGER_DISPLAY_REFRESH_AUTO_CADENCE` | Derive the draw cadence from the probed refresh rate (on/off). | | `OC_PAGER_SCROLL_DEBUG` | Show the scroll debug HUD. | | `OC_PAGER_SCROLL_LOG` | Log scroll input events (unset or `0` = off). | | `OC_PAGER_FPS` | Show the frames-per-second HUD. | | `OC_PAGER_MEMTRACE` | Periodic memory trace in the debug log; `0`/`false`/`off` disables. | | `OC_PAGER_MEMTRACE_INTERVAL_SECS` | Memory-trace interval (seconds). | | `OC_PAGER_MEMTRACE_THRESHOLD_MB` | Memory trace logs only above this resident size (MB). | | `OC_PAGER_MOUSE_REPORTING_TOGGLE` | Enable the mouse-reporting toggle (on/off). | | `OC_PAGER_SHOW_THINKING_BLOCKS` | Show model thinking blocks (on/off; overrides the setting). | | `OC_PAGER_GROUP_TOOL_VERBS` | Group consecutive tool calls by verb (on/off; overrides the setting). | | `OC_PAGER_SHOW_EDIT_DETAILS` | Show each edit's diff inline (on/off; overrides `[ui] show_edit_details`). | | `OC_PAGER_SHOW_RUN_DETAILS` | Finished agent Runs show the command and first output lines (on/off; overrides `[ui] show_run_details`). | | `OC_PAGER_COLLAPSED_EDIT_BLOCKS` | Legacy: start edit blocks collapsed (on/off); read only when `show_edit_details` is unset everywhere, as its inverse. | | `OC_PAGER_CONTEXTUAL_HINTS` | Contextual hints on/off (overrides `[ui.contextual_hints]`). | | `OC_PAGER_TIPS_OVERRIDE` | Debug builds only: `|`-separated tips that replace the configured ones. | | `OC_PAGER_SLASH_COMMAND_TAGS` | JSON map of tags shown beside slash commands in the `/` menu. | | `OC_PAGER_DEFAULT_SELECTED_PERMISSION` | Which permission-card option the cursor starts on. | | `OC_PAGER_ESC_DOUBLE_PRESS_MS` | Window for the double-Esc gesture (ms). | | `OC_PAGER_DOCK` | The dock above the prompt on/off; on by default (`0` brings back the Tasks pane at the top; a requirement pin wins, `[features] dock` in the pager config comes after). | | `OC_PAGER_DOCK_V2` | Same switch as `OC_PAGER_DOCK`, read after it. | | `OC_PAGER_AGENT_DASHBOARD` | `0` turns `/dashboard` off. | | `OC_PAGER_WORKSPACE_DASHBOARD` | Use the workspace (v2) dashboard layout instead of the persisted v1 layout. | | `OC_PAGER_SESSION_PICKER_GROUPED` | `1`/`0`: group the session picker (overrides `[cli] session_picker_grouped`, default on). | | `OC_PAGER_HUNK_TRACKER` | Hunk-tracker mode (overrides `[ui] hunk_tracker_mode`). | | `OC_PAGER_SUGGESTIONS` | Shell-command ghost suggestions (off by default). They request `ocean/suggest`, which the Ocean engine does not serve, so nothing shows. | | `OC_PAGER_SUGGESTIONS_AI` | AI variant of `OC_PAGER_SUGGESTIONS` (same limitation). | | `OC_PAGER_SUGGESTIONS_AI_MODEL` | Model id for `OC_PAGER_SUGGESTIONS_AI`. | | `OC_PAGER_MCP_PUSH_SERVER_STATUS` | Push MCP server status updates into the UI (default on). | | `OC_PAGER_ENVRC_TIMEOUT_SECS` | Timeout for evaluating a directory's `.envrc` (seconds; `0` disables). | | `OC_PAGER_CONNECT_UI_TIMEOUT_SECS` | Seconds before startup shows the connect-failure report (6 s floor). | | `OC_PAGER_PROMPT_ACK_TIMEOUT_SECS` | Prompt acknowledgement timeout (seconds). | | `OC_PAGER_EXIT_TIMEOUT_SECS` | Force-exit this many seconds after a quit if teardown hangs (default 20; `0` disables). | | `OC_PAGER_COPY_FILE` | Backup file every copy is also written to (default `<pager home>/last-copy.txt`). | | `OC_PAGER_OSC52_SINK` | Emit clipboard writes as OSC 52 for a wrapping terminal. | | `LC_OC_PAGER_OSC52_SINK` | Same as `OC_PAGER_OSC52_SINK`, SSH-forwardable. | | `OC_PAGER_CLIPBOARD_NO_OSC52` | Never emit OSC 52 (for hosts that paint it as garbage). | | `OC_PAGER_CLIPBOARD_NO_NATIVE_READ` | macOS: don't read the native pasteboard. | | `OC_PAGER_CLIPBOARD_NO_DATA_CONTROL` | Linux: skip the Wayland data-control clipboard protocol. | | `OC_PAGER_SQLITE_JOURNAL_MODE` | SQLite journal mode for the pager's local databases (the WAL kill switch). | | `OC_PAGER_RESET_CHILD_OOM` | Linux, internal: set on child processes so they reset the inherited OOM score. | | `OC_PAGER_EVENT` | Set for notification hook commands: the event name. | | `OC_PAGER_MESSAGE` | Set for notification hook commands: the message text. | | `OC_PAGER_SESSION_ID` | Set for notification hook commands: the session id. | | `OC_PAGER_BUNDLE_RG_PATH` | Build time: embed this ripgrep binary instead of downloading the release tarball. | | `OC_PAGER_RG_VER` | Build-internal: embedded ripgrep version, set by `ocean-pager-ripgrep/build.rs`. | | `OC_PAGER_RG_TARGET` | Build-internal: embedded ripgrep target triple. | | `OC_PAGER_RG_SHA256` | Build-internal: SHA-256 the extracted ripgrep is verified against. | | `OC_PAGER_CHANGELOG_OFFLINE` | Tests: never fetch the changelog. | | `OC_PAGER_MDM_NO_EXPAND_TEST` | Tests: macOS managed-preferences layer. | | `OC_PAGER_TEST_OPEN_URL_FILE` | Tests: write opened URLs to this file instead of launching a browser. | | `OC_PAGER_TEST_FRAME_WRITE_DELAY_MS` | Tests: delay each frame write (ms). | | `OC_PAGER_TEST_HOLD_TEARDOWN_SECS` | Tests: hold teardown to exercise the exit timeout. | ## What is wired to the Ocean engine `/`, `/mcps`, `/skills`, `/plugins`, `/hooks`, `/workflows`, `/model`, and `/effort` use the pager chrome and Ocean `Services`. MCP add/toggle, plugin enable/disable, hook reload, and tool permission cards go through the same ACP methods Grok Build uses. The `/workflows` picker lists built-in and discovered project/user workflows over `ocean/workflows/list` (empty when the `workflows` section is disabled); launching, pausing, resuming and stopping a run go through the `workflow` tool, not an ext request. **Skills complete in the composer.** Typing `/` and a prefix lists your **enabled**, user-invocable skills alongside the pager's own commands — a plugin skill under Claude Code's plugin-qualified name (`/superpowers:writing-plans`, so `/super` finds every superpowers skill), a project or user skill under its bare name (`/formatcode`); the prefix narrows the list, `Tab`/`Enter` inserts `/<skill> ` into the message box so you can keep typing, and sending dispatches it — the engine expands the skill body into the turn (a typed bare `/writing-plans` still runs the plugin skill too). The candidates come from `ocean/skills/list` (fetched at session start; the Ocean engine does not advertise skills as ACP commands), so a **disabled** skill is never offered. A `Space` toggle on the Skills tab of the capabilities picker updates the popup live, and the `<available_skills>` block and `skill_*` tools shrink on the next step ([09 · Capabilities](/oceancode/capabilities#enabling-and-disabling-skills)). **The capabilities picker.** Each tab (`/skills`, `/plugins`, `/hooks`, `/mcps`, `/workflows`, Marketplace) lists only its own kind. On the Skills tab, `Space` toggles the skill under the cursor and the change sticks (it writes `~/.oceancode/skill-state.json`); `/` opens a search that filters the rows by the skill's name, its slash id, its description and its author, case-insensitively, and a query that matches nothing reads `No matches`. `a` / `d` enable / disable every skill behind a confirm prompt. Each row shows its source — `project`, `user` or `plugin: <name>` — grouped by source. **Group and master toggles.** On the Skills, Plugins, Hooks and MCP tabs `Space` also works above the single row; every change applies on the next step and costs one cache rewrite ([09 · Capabilities](/oceancode/capabilities#enabling-and-disabling-skills)): | Row / key | Action | |---|---| | `Everything` (first row) + `Space` | Master switch: off removes skills, MCP tools, hooks and plugin rules from the agent's context, built-in tools stay; asks first | | `All skills` / `All plugins` / `All MCP` / `All hooks` (second row) + `Space` | The whole kind on or off; asks first | | `a` / `d` (Skills tab) | Same as `All skills` on / off | | A `Plugin: <name>` header (Skills, MCP) or a hook source header + `Space` | Every row in the group; asks first above 10 rows; an all-off group reads `[off]` | | A row + `Space` | That one skill, plugin, hook or MCP server | The picker's last row shows the live counts (`skills 22/259 · plugins 2/21 · mcp 1/10 · hooks 8/8 · applies on the next step (one cache rewrite)`), prefixed `EVERYTHING OFF` while the master switch is off. The Plugins tab also lists Claude Code's cached plugins under `User (Claude)`. The **Marketplace** tab reads `~/.oceancode/marketplaces/**` only when you open it (opening `/skills` or `/plugins` no longer touches the marketplace), and marketplace content never reaches the prompt or the tool list. ## Pasted images Paste an image from the clipboard (`Ctrl+V` / `Cmd+V`) or drag an image file onto the composer; it shows as `[Image #1]` in your text and goes to the model with the prompt ("what's wrong with this layout? [Image #1]"). Every route carries it — Anthropic, OpenAI chat and Responses (xAI, Groq, OpenRouter, …), Gemini. With several images, each is labelled `[Image #N]` in the request, so the model matches the name in your text to the right picture. Limits: at most 20 images per prompt, **3.75 MiB** per image once stored (about 5 MB once base64-encoded, the strictest limit of the routes), and **8000 px** on either side (Anthropic refuses a larger image before the model runs; `sips -Z 2576 <file>` brings it under). The engine sends PNG, JPEG, GIF and WebP — the types every route accepts, decided by the image's bytes, not its declared type. The pager re-encodes a TIFF (what a macOS screenshot usually is on the clipboard), BMP, ICO or GIF as PNG before it sends, so those paste and drag in like any other; PNG, JPEG and WebP go as they are. An image over the limits after that, a type the pager cannot decode, or an `http(s)` source refuses the whole prompt with a message naming the image; nothing is dropped silently. **Dragging from another app.** In Ocean AI Terminal a file dragged in from Photos or Finder becomes a chip the same way, and since desktop build 0.8.11 the drop also brings the terminal window to the front: macOS does not activate the window a drag lands on, so with an older build the keys you type next, Enter included, still go to the app the image came from until you click the terminal once. **Resizing.** Images are resized so they never make a session fail (the same rules apply to images the `read` tool returns): - An image over **2576 px** on its long edge (the most a current Claude model reads; larger is downscaled by the provider anyway) is shrunk to 2576 px when it is pasted or read. The resize is Lanczos3 with the aspect ratio kept; PNG stays PNG and JPEG stays JPEG, GIF and WebP become PNG. - When one request carries **more than 20 images**, Anthropic applies a stricter size limit to every image in it, so any image over **2000 px** is sent as a 2000 px copy. The stored session is not changed. - If the provider still refuses an image for its size, the engine retries once with every image at or under the limit the provider named, and keeps that limit for the rest of the session. - Past **100 images** or **24 MiB** of image data in one request, the oldest images are replaced by a one-line note (`[<name>, <W>x<H> omitted from this request: …; read the file again to see it]`). **Text-only models.** When the catalog says the current model reads no images (its models.dev entry lists input modalities without `image`), the text still goes and the image does not; a session note says so: `Image #1 not sent: <model> doesn't accept images`. A model the catalog cannot describe — an offline seed entry or one you declared under `providers.*.models` — gets the image. Switching to a text-only model after pasting drops the media from the request on the next turn — and an image a tool returns (`read` on a PNG) is replaced by a one-line note on such a model — so the provider does not answer 400. The image is stored inline on the user row of `oceancode.db` (`"files":[{"uri":"data:image/png;base64,…`); a reopened session replays that row as text only, and the headless and server prompt bodies ([07 · Headless and server](/oceancode/headless-and-server)) carry text only for now. ## The dock: sub-agents above the prompt Running sub-agents, background commands, monitors, loops and queued prompts live in one dock directly above the prompt, one section each (`Agents`, `Tasks`, `Watchers`, `Queued`). While a turn runs, its status rides the dock's first header instead of taking a row of its own: ``` ▾ Agents 2 ⸬ Working 2m31s 1m20s ⇣12k [stop] ⸬ Find every caller of resolveWorkflowAccess — Running: rg -n … 1m15s silent · 4 tools · 38k tok · 2m13s ✓ Map the picker code paths 9 tools · 52k tok · 48s ``` A sub-agent row is the task's description, led by a badge only when the mission gave the sub-agent a persona or role (`Researcher`), then its live activity; the right column shows its tool calls, tokens and elapsed time, plus `Ns silent` once it has sent nothing for 10 s — amber from a minute, the one to open. The dock counts the agents, so the Ocean bar does not. `Ctrl+G` hides and shows the dock; a section folds on `Enter` or a click on its header, `show N more` opens a long one, `↗` opens a row and `[stop]` stops it. One blank row separates the transcript from what sits under it; nothing else is spaced. The transcript keeps a record: `Subagent running: "…"` when a sub-agent starts and `Subagent completed in 48s: "…" · 52k tok · 9 tools` when it ends; the running line does not repeat the dock's live figures. `OC_PAGER_DOCK=0` (or `[features] dock = false`) brings back the Tasks pane at the top, the live figures on the transcript row and the `agents N` count in the bar. Under a work mode with an at-once limit ([23](/oceancode/work-modes#limits)), a sub-agent that must wait for a free slot gets a dock row of its own with a hollow glyph, `waiting (4/4 running)`, which turns into its running row when a slot frees (or closes if the turn is stopped first). Model, the default, has no such limit, so this row never appears there. ## The work-mode picker (`F4`) `F4` or `/mode` opens the work-mode picker: who decides how agents work together — the model (Model, the default), Jev (Auto), or you (seven diagrams). The list sits on the left and the highlighted mode's picture on the right; moving the cursor redraws it at once. `Enter` uses the mode for this session, `d` makes it the current agent's default, `e` copies a built-in into `.oceancode/modes/` to edit, `Esc` closes. `/mode <id>` and `/mode default` do the same without the picker. Every mode but Model shows its badge on the prompt's top border, after the `ultracode` badge when that is on (`delegate ≤4`, `race ×3`, `auto → goal · Jev 0.82`); Model shows none, so the border is unchanged. F2 › Agent defaults holds a mode row per ring agent, and F2 › Sub-agents the three sub-agent limits ([22](/oceancode/pager-reference#editor-agent-and-models)). The modes, limits and requirements are on [23 · Work modes](/oceancode/work-modes). ## Watching a sub-agent Open a sub-agent with a double-click or `Enter` on its dock row (`↗`), its transcript row or its Agent Dashboard row: the view fills with what the sub-agent does as it does it — its bash commands and their output, reads, greps, edits with their diff, and its text and thinking — with its own context bar. `q` or `Esc` closes it. The engine streams a running sub-agent's activity live under the sub-agent's own session id, while the parent's turn runs and afterwards for a background sub-agent. A finished sub-agent whose view holds nothing yet — or whose view was freed after you closed it — is rebuilt from `oceancode.db` when you open it (`ocean/session/replay`). Sub-agents of a session you resumed, or of an earlier pager run, do not open yet. ## Goal mode (`orchestrate: always`) Set `orchestrate: "always"` in the `workflows` section of `oceancode.json` and a plain prompt is no longer a single turn — it launches the built-in `goal` workflow, which plans the objective into verifiable deliverables, works in rounds with one resumed worker, and converges only when a panel of adversarial skeptics fails to refute completion (identical gaps two rounds running, or a spent budget, pause instead of looping). The pager's Workflows dock shows the run advancing through its **Plan → Work → Verify** phases, with the sub-agents of each round beneath it. The Claude-settings `ultracode` flag does **not** turn this on — only the explicit `orchestrate` opt-in does. Headless, the same goal run is `oceancode goal "<objective>"` (see `07-headless-and-server.md`). Since work modes, `orchestrate: "always"` with no `mode` set is read as the Goal work mode ([23](/oceancode/work-modes)). ## What the pager does not do Login, billing, worktrees, `/imagine` and Grok memory flush are unsupported (`-32601`). Connecting a provider is in the pager: pick a gray `/model` row, or `/connect <provider>`. OpenCode-style auth methods: - **API key** — most providers (Google, Groq, Perplexity, …). - **ChatGPT Plus/Pro** — OpenAI, via OpenCode’s JSON deviceauth (`auth.openai.com/codex/device`), or paste an API key. - **Claude Pro/Max** — Anthropic PKCE (paste the browser code), or paste an API key. - **GitHub Copilot** — GitHub device flow (`github.com/login/device`). - **claude-code** — `oceancode claude` (Claude CLI login). - **Bedrock / Vertex** — environment/config, not a prompt. Already-connected providers open a manage overlay: switch API key ↔ subscription, or **d** to disconnect (removes `auth.json`; env-backed providers stay connected until the env var is unset). Credentials go to the same `auth.json` as `oceancode key` / `oceancode login`. ## Expired subscription Ocean runs on the Claude subscription by consuming the active slot Claude Code keeps in `~/.claude/.credentials.json` (`claudeAiOauth`); it never rotates that slot's refresh token. On macOS, once that file's token is within five minutes of expiry, Ocean also reads the Keychain item Claude Code refreshes itself into (`Claude Code-credentials`, read only) and uses whichever carries the later expiry, so a self-refresh by Claude Code no longer strands Ocean an hour later; no file still means no login. When the slot's access token has expired and carries no refresh token, a prompt cannot be sent, and the turn fails in the transcript with a **terminal, actionable** message — `Claude subscription token has expired: Run oceancode login to reconnect your Claude account.` — and **no** retry countdown or network banner (the failure is not a transport error and is never retried). Run `oceancode login`, complete the Claude Pro/Max browser flow, and send the prompt again. (Headless, `oceancode run` prints the same message to stderr and exits non-zero.) The same holds when the slot expires **mid-session**: it was valid when the turn started, so the request reached Anthropic and the server rejected the now-dead token with an HTTP 401. On the subscription route that 401 (and a 403) is the same terminal `run oceancode login` failure — it is never retried — whereas a 401 on an API-key provider stays a generic error (its fix is a new key, not a re-login). ## Coming from the native TUI (removed 2026-10-05) The pager renames some surfaces of the removed native TUI and has no `ctrl+x` leader — it uses direct chords. The pager equivalents: | Native TUI | Ocean pager | Notes | |---|---|---| | `/copy` | `/toggle-mouse-reporting` | Toggle terminal mouse capture so text selection/copy works. | | `/mcp` | `/mcps` | The MCP-servers picker. | | `/models` | `/model` | The model picker (also gates provider `/connect`). | | `/cockpit` | status bar + `/context` | No unified cockpit panel; the status bar plus `/context` show the same figures. | | `ctrl+x` leader chords | direct chords | The pager has no `ctrl+x` prefix. Use `ctrl+p` (command palette), `ctrl+\` (Agent Dashboard), `ctrl+t` (todos pane), `ctrl+m` (multiline / model picker off the prompt). | ## Shift+Tab ring and permission modes `Shift+Tab` cycles Build → Auto → Plan → Ask → Debug → Build. Build, Plan, Ask and Debug are the engine's agents (the curated ring in `crates/ocean-acp/src/modes.rs`); Auto is the one permission stop, Claude Code's "accept edits": the agent stays Build, the prompt border shows `auto` in yellow, and the engine's permission mode is `acceptEdits` — edits run without a card, bash and outside-directory asks still prompt (or go to Jev, see [13](/oceancode/jev-decisions)), and the `question` tool always asks. Cycling on from Auto drops back to ask before the switch to Plan. The stop is absent while always-approve is on, and when the auto feature is gated off (`[auto_mode] enabled = false` in the pager config). The same mode is reachable from `/auto`, `/always-approve` (`ctrl+o`; the engine's `bypassPermissions`, every card auto-answered by the pager) and the `permission_mode` row of the settings modal. Each change is persisted to the pager config and sent to the engine as `ocean/yolo_mode_changed`, so the engine's mode and the border flag agree (see [12 · Permissions and sandbox](/oceancode/permissions-and-sandbox#permission-modes)). A subagent's ask surfaces as its own card, labelled with the subagent. The agent is the bold flag on the prompt's bottom border (`Build` blue, `Plan` violet, `Ask` green, `Debug` orange); the permission mode follows it (`auto` yellow, `always-approve` red). The Plan agent is plan mode, so it shows once, as `Plan`; `plan approval` and `commenting L3` appear beside it while a plan is under review. Each agent can carry a default model and effort (`agents.<id>.{model,effort}` in `oceancode.json`, edited under F2 › Agent defaults): Shift+Tab to that agent puts the session on them, the effort clamped to the levels the model offers, and status-bar row 1 follows. `Option+Tab` (or `F3`, for terminals that don't send Option as Meta) steps the session's effort up through the current model's levels, wrapping to the lowest, and prints the same "Switched to …" line as `/effort`. That change lasts for the session: the next Shift+Tab loads the agent's own default again. An agent with no defaults keeps the session's model and effort. A settings push reaches an open session live — a fleet-router Apply, an F2 edit, or a hand edit of `oceancode.json` or `~/.claude/settings.json`. The pager shows one toast naming what moved, for example `Settings applied: sub-agents effort medium · build effort max`, and status-bar row 1 shows the new values. The values it names are the main model and effort, ultracode, the sub-agent model and effort, FORCE, and each agent's model and effort. When the push changes the row of the session's current agent and the session still runs that row's old value, the session moves to the new one. A Mesh › Main change moves a session on the previous Main model unless its agent has a row of its own: the row wins (today that leaves Debug, which has no row). A model or effort you changed by hand in this session (`/model`, Option+Tab) is kept. ## Question cards When the agent asks with the `question` tool, a card replaces the composer: `↑/↓` walk the answers, `Enter` picks one (or starts typing on the last row, where you answer in your own words — that text reaches the model as written), `←/→` move between questions. - **`Ctrl+B` back to chat** leaves the question to give the agent more first — notes, pasted images, links. The turn stops, the answers you had picked land in the composer as `My answers so far:`, and the agent is told you stopped to add context; send your message and it thinks again and asks only what is still open. - **`Esc`** steps back (out of the text row, then clears a pick); **`X`** dismisses the card without answering; **`Ctrl+C`** cancels the turn and the open question with it. A second `Ctrl+C` while the turn is still cancelling arms quit, and a third quits. ## Resuming: the session tree `/resume` (and the home screen's list) shows your **main sessions** only, most recently active first. A session that spawned sub-agents carries a fold glyph and says how many in the right column — `› Pager fixes 3 sub · 5m ago` — and the sub-sessions stay out of the way until you open it: - **`→`** (or `e`) on a parent row opens it: its sub-sessions are fetched the first time and drawn indented underneath; a sub-session that spawned its own folds the same way, as deep as the work went. On a row with nothing to open, `→` shows the detail card as before. - **`←`** closes the detail card if it is open, then folds the sub-sessions away, then moves the selection up to the row's parent. - **Typing** searches titles on the engine, across main sessions and sub-sessions alike. A hit is drawn under its main session, with the rows between them dimmed when they did not match, so you can tell which run it belongs to. Clearing the search brings the folded list back. - A row badged **`open`** is already running in another pager window on this machine; open it there instead of resuming it twice. - `Enter` resumes whichever row is selected, a sub-session included. The engine names a session after the first prompt that says something — `Hi`, `Yeah`, `Do it`, `Continue`, `[Image #1]` and the like name it only until a real request arrives, so a list of greetings does not happen. `/rename` still wins over any automatic title. ## Search counter and tideline tiers The top-right status row shows a `search N · 0.3s` chip once the session has run a `grep` or `glob` tool call: `N` is the number of calls, the time is the last call's latency (green ≤ 1 s, yellow ≤ 5 s, orange above). Click it for the breakdown — calls by tool, last / average / total latency, bytes and files scanned, and how many recursive `grep -r` / `find` the model ran through bash (and how many of those were rewritten to `rg`). The figures are folded from each tool result's metadata (`elapsed_ms`, `files`, `bytes`, `search_redirect`) in `oceancode.db`, so they survive a restart; nothing is counted in memory. The Ocean context row (`ctx … N% full · tideline …`) is tiered against the tideline limit: green under 80 % of it, yellow from 80 %, orange at or over it (the gauge, the percentage and the tideline clause share the tone). With the ledger off the same tiers apply against the context window. `tideline ×12 @30% …` carries the number of turns past the tideline (`/tideline` prints the same figure); no `×N` while it is zero. The `bash` tool rewrites a plain recursive `grep -r …` to the bundled ripgrep (POSIX shells; every flag must have an `rg` equivalent, patterns are literal-safe) and prepends `[oceancode] ran as: rg …` to the result; any other recursive `grep` or a `find … -name` runs as typed under a one-line hint naming the grep / glob tools. A leading `cd <dir> &&`/`;` prefix, a trailing `2>/dev/null`, `| grep -v …` / `| head` followers, and BRE `\|` alternation are carried or translated verbatim, so the common slow pipelines still rewrite. The ripgrep directory leads the child `PATH`, so `rg` is available in bash on every OS where a binary resolves (`OC_RG_BIN` overrides; see 02). ## Ocean session commands These act on the Ocean engine over `_ocean/*` / `ocean/*` ext methods (Ocean backend only): | Command | What it does | |---|---| | `/tideline` | Bare shows the ledger state; `/tideline now` cuts the ledger; `<n>%`, `<tokens>`, or `default` set the tideline (the floor and the presets were removed — a second number or a preset word is refused with the usage line). The old `/shelve` still works as a hidden alias. | | `/retention` | Bare shows how long idle sessions are kept and what the next sweep would delete (`retention 7 days · 12 sessions pending · first sweep after <date> (grace)`, or `… · last sweep 5m ago, 340 swept`); `<days>` sets `retention.days` in the global config and re-arms the sweep at once; `off` is `0` (keep everything). An out-of-range number is refused with the engine's message. While sessions wait out the 24-hour grace window the context row shows `retention N pending` and one notice is toasted. See [18 · Operations](/oceancode/operations#retention-and-vacuum). | | `/budget` | Bare shows the spend cap; `$5` (or `5`) caps USD, `off` clears it, `subscription` switches to list-price mode. | | `/limits` | Bare shows the per-turn bounds; `steps <n>` caps steps, `time <90m\|1h30m>` bounds wall-clock. | | `/subagent` | Bare reports the effective sub-agent model + source; `<provider/model>` pins one, `default` / `off` / `clear` removes the override. | | `/mode` | Bare opens the work-mode picker (as `F4`); `<id>` sets this session's work mode; `default` clears it, so the agent's or the config's mode applies again. Answered with a toast, `Mode: <label>`. See [23](/oceancode/work-modes). | | `/plan add\|done\|cancel\|edit\|remove` | Edit the live todo table (1-based rows); bare `/plan` still enters plan mode. | | `/redo` | Re-apply the turn a `/undo` (`/rewind`) reverted, and the files it put back. | | `/share` | Share this session via a public URL (Ocean backend). | | `/unshare` | Revoke the current session's share link. | | `/notify` | Toggle desktop notifications; `/notify on\|off` sets it explicitly. | # Xcode and other ACP editors — oceancode acp Source: https://www.plungeai.com/docs/oceancode/xcode `oceancode acp` runs the Ocean engine as an **Agent Client Protocol** (ACP) agent on the process's stdin/stdout. An ACP editor — Xcode 26.6 and later, Zed, JetBrains, Neovim — starts the binary itself as a subprocess and talks newline-delimited JSON-RPC over the pipe. There is no URL, no port and no API key for the editor: the editor is the client, the subprocess is the agent, and the editor asks you for consent the first time it launches it. It uses the same ACP backend and engine as the pager: the same model and credentials, permissions, agents (build · plan · ask · debug), ledger and session store. The editor launches `oceancode acp` directly; it does not route requests through the pager or require an HTTP server. ACP agent and Ocean extension handlers live in `crates/ocean-acp/`; the pager is another client that embeds the backend in-process (`crates/ocean-cli/src/acp.rs`, `crates/ocean-acp/src/stdio.rs`). | Flag | Default | Meaning | |---|---|---| | `--db <path>` | `oceancode.db` | the session store | | `--directory <dir>` | cwd | the working directory the engine starts in; the editor's `session/new` `cwd` wins per session | | `--verbose`, `-v` | off | trace every provider request/response to stderr (credentials masked) | **stdout is the wire.** Every byte the process writes to stdout is one JSON-RPC message; the banner, diagnostics and tracing go to stderr. The process exits 0 when the editor closes stdin. ## Xcode Source: Apple, [Setting up coding intelligence](https://developer.apple.com/documentation/xcode/setting-up-coding-intelligence) — "To enable an agent that doesn't appear in Intelligence settings and supports the Agent Client Protocol (ACP), click Add an Agent under Agents, enter information in the next sheet, and click Add." 1. Find the binary's absolute path: `which oceancode` (for example `/Users/you/.local/bin/oceancode`). Xcode needs the absolute path — no `~`, no relative path, and it validates nothing until first launch. 2. **Xcode › Settings › Intelligence**, under **Agents** click **Add an Agent…** and fill the sheet: | Field | Value | |---|---| | Name | `Ocean Code` | | Executable | the path from step 1 | | Interpreter | leave blank (`oceancode` is a native binary, not a script) | | Arguments | `acp` — one argument per row | | Environment Variables | none needed (see Credentials) | Click **Add**. Ocean Code now appears under Agents. 3. In the Intelligence sidebar click **New Conversation** and pick **Ocean Code**. The first time, Xcode asks whether to allow the agent to run — click Allow. Do not confuse the Agents sheet with **Chat › Add a Chat Provider**: that door takes a URL (or a local port) serving `/v1/models` and `/v1/chat/completions` plus an API key and gives a chat box without tools. The coding agent lives behind the Agents sheet. ## Credentials Xcode starts the agent through your login shell (observed on Xcode 27: the agent's `PATH` carries `~/.local/bin`, Homebrew and Xcode's `Developer/usr/bin`), so MCP servers started with `npx` or `uvx` spawn fine — but do not rely on a shell `export` for the credential. Log in once from a terminal and the stored credential is used by every Xcode session: ```bash oceancode auth # API key or Claude Pro/Max subscription → auth.json in the data directory ``` ([01 · Getting started](/oceancode) lists the locations.) A key in `oceancode.json` works too. The sheet's Environment Variables rows also work, but Xcode writes their values into a plist under `~/Library/Developer/Xcode/CodingAssistant/ACP/` — prefer the stored login. Xcode runs the agent with `~/Library/Developer/Xcode/CodingAssistant/<agent-id>/` as its working directory, so that is where `oceancode.db` — the transcripts of your Xcode conversations — lands; each session's own `cwd` is the project folder Xcode hands over in `session/new`. ## Xcode's tools When Xcode starts a session it hands the agent its own tools — build, test, run, SwiftUI previews, documentation search, diagnostics — as an MCP server (`xcode-tools`) in `session/new`. The engine registers every server the editor supplies before the session is created (`crates/ocean-acp/src/mcp_servers.rs:19`, `crates/ocean-acp/src/agent.rs:158`), so the first step already offers them; the tools are named `xcode-tools_<Tool>` in the registry. Nothing to configure: "build the project" in an Xcode conversation uses Xcode's build, and Xcode asks before the first build/test tool runs, as it does for every agent. For a session you start **outside** Xcode — the pager, `oceancode run` — the same tools come from Xcode's MCP bridge: in Intelligence settings turn on **Allow external agents to use Xcode tools**, then ```bash oceancode mcp add xcode --command xcrun --arg mcpbridge ``` with the project open in Xcode ([11 · MCP](/oceancode/mcp); Apple: [Giving external agents access to Xcode](https://developer.apple.com/documentation/xcode/giving-external-agents-access-to-xcode)). ## Permissions and questions A tool call that needs approval becomes an ACP `session/request_permission` with allow once, allow always, and reject choices. The shared outbound driver resolves replies and rejects them on cancellation or disconnect (`crates/ocean-acp/src/outbound.rs`, `permission.rs`). The engine's questionnaire tool has no equivalent in base ACP. A client that advertises Ocean's questionnaire extension receives the structured question request; a generic ACP client receives the question text in the transcript and the blocked tool is rejected so the model can continue conversationally. This fallback avoids claiming that an unsupported form was shown. Xcode's actual questionnaire behavior has not yet been validated in its UI. Always-approve and the Shift+Tab agent ring are reachable through the editor's mode picker where it has one; Xcode exposes the session modes it is given (build · plan · ask · debug). ## Verify by hand Two requests on stdin, one reply or notification per line on stdout, exit 0 at EOF — no Xcode needed. Hold stdin open for a moment: the host returns as soon as the pipe closes, and a request still in flight at that instant is dropped (an editor keeps the pipe open for the life of the conversation). ```bash { printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}' \ '{"jsonrpc":"2.0","id":2,"method":"session/new","params":{"cwd":"'"$PWD"'","mcpServers":[]}}' sleep 3 } | oceancode acp 2>/dev/null ``` The first line answers with `agentCapabilities` (`loadSession`, `mcpCapabilities.http/sse`), the last with a `sessionId` plus the model and mode lists; between them an `_ocean/sessions/changed` notification — an Ocean extension the pager uses and a plain ACP client ignores. Add a `session/prompt` line with that id to see the turn stream as `session/update` notifications. This is what `crates/ocean-cli/tests/acp_mode.rs` pins on every build. ## See also - [19 · Ocean pager](/oceancode/pager) — the terminal UI on the same engine. - [07 · Headless and server](/oceancode/headless-and-server) — `serve`, `run`, `goal`, `workflow`. - [12 · Permissions and sandbox](/oceancode/permissions-and-sandbox) — the rules behind every approval prompt. # CLI reference Source: https://www.plungeai.com/docs/oceancode/cli-reference `oceancode` is one binary: `oceancode [subcommand] [flags] [args]`. With no subcommand it starts Ocean pager. `acp` runs it as an editor agent, and `serve`, `run`, `goal` and `workflow` run it headless. [01 · Getting started](/oceancode) explains the three boot shapes. This page lists every subcommand, its flags and its exit codes. ## Flag syntax Most engine commands parse flags with a port of Go's `flag` package: the default shape, `serve`, `acp`, `migrate`, `run`, `workflow`, `goal`, `ledger`, `export`, `login`, `logout`, `key` and `auth`. Go's rules apply: - `-name` and `--name` are the same flag. A value is written `--name value` or `--name=value`. Single-letter flags cannot be bundled: `-vc` is one flag named `vc`. - Parsing stops at the first argument that is not a flag, or at a bare `--`, which is dropped. Everything after that point is positional, even if it looks like a flag. - A boolean flag never takes the next argument; turn one off with `--name=false`. Integer flags accept Go's base prefixes (`0x1000`, `0o10000`, `0b…`) and `_` separators. - `-h`/`--help` prints `Usage of <name>:` and the flag list to **stderr**, then exits **0**. An unknown flag, a missing value or a bad value prints the error and the usage to stderr, then exits **2**. The capability and maintenance commands (`plugin`, `mcp`, `skill`, `trust`, `hooks`, `jev`, `inspect`, `doctor`, `update`, `verify-release`, `uninstall`, `claude`) parse their arguments by hand. Write their flags with two dashes, exactly as shown. Most of them reject `--help` with a usage error (exit 1). **`update` ignores `--help` and runs a real update**, so never try `oceancode update --help`. ## Global flags `--version` must be the first argument. `--data-dir` and `--fake-llm-sse` are removed from the whole command line before the subcommand is chosen, so a `run` prompt that contains the words `--data-dir x` loses them. | Flag | Type | Default | Meaning | |---|---|---|---| | `--version` | — | — | Print `oceancode <version> (<build stamp>)` and exit 0. Needs no database, config or terminal. `-v` is **not** an alias: it means `--verbose`. | | `--data-dir <dir>` | string | the app data dir | Where `auth.json` and the other per-install state live, for commands that boot the engine. Also `--data-dir=<dir>`. | | `--fake-llm-sse <file>` | string, repeatable | none | Test hook: replay a recorded provider SSE body for each turn, in order, repeating the last one. Also `--fake-llm-sse=<file>`. | `OC_DETERMINISTIC=1` is read at the same point: it pins the clock and seed that ids are minted from, so two runs produce identical id sequences. Every invocation except `--version` checks for orphaned sessions left by a crashed run. When it finds any, it prints one line to stderr: ``note: <n> orphaned session(s) from a previous run — run `oceancode doctor` for details``. ## Common engine flags `--db`, `--directory` and `--verbose` are **not** global; each command declares them. They mean the same thing everywhere, so the per-command tables below omit them. | Flag | Type | Default | Meaning | Taken by | |---|---|---|---|---| | `--db` | string | `oceancode.db` | Path to the session database. | the default shape, `run`, `serve`, `acp`, `workflow run`/`pause`/`resume`/`stop`, `goal`, `ledger`, `export`, `migrate` | | `--directory` | string | current directory | Working directory. For `workflow runs` it is the project to filter by. | the same, minus `migrate`, plus `workflow runs` | | `--verbose`, `-v` | bool | off | Trace every outbound LLM provider request/response to stderr, with credentials masked. | the default shape, `run`, `serve`, `acp`, `workflow run` and its control verbs, `goal` | ## Exit codes | Code | Meaning | Where | |---:|---|---| | 0 | Success, or `-h`/`--help` on a Go-flag command. | all | | 1 | The command failed. The message is printed to stderr as `error: <message>`. | all | | 2 | Flag parse error (unknown flag, missing or bad value). | Go-flag commands | `oceancode run` exits with a **typed code** once its turn has started. A failure before that (boot, flag validation, an empty message) is exit 1. A turn that ends while background sub-agents it started still work does not end the run: `run` waits for them and for the turns their results start, and exits on the last. | Code | Meaning | Stop reason(s) | |---:|---|---| | 0 | finished | `finished`, `waiting` | | 2 | permission rejected | `permission_rejected` | | 3 | budget | `budget` | | 4 | time | `time` | | 5 | no progress | `no_progress`, `max_steps`, `doom_loop` | | 6 | error | `error`, `hook_blocked`; also a `--json-schema` that cannot be satisfied | | 130 | interrupt | `user_interrupt` | `oceancode workflow run` and `oceancode goal` exit by the run's final status: | Code | Status | |---:|---| | 0 | `complete` (and a `--validate-only` check) | | 2 | `failed`, `cancelled`, `interrupted`, `budget_limited`, or any unrecognised status | | 3 | a pause: `user_paused`, `back_off_paused`, `no_progress_paused`, `infra_paused`, `blocked` | Exit 2 from `run` can be a permission rejection or a flag error; the stderr text tells them apart. ## `oceancode` (no subcommand) ```bash oceancode [--db <path>] [--directory <dir>] [--listen <host:port>] [--tui pager] [-v] ``` Execs Ocean pager, which runs the engine inside its own process. stdout must be a terminal; otherwise it fails with ``stdout is not a terminal — use `oceancode serve` for headless serving``. The pager binary is looked up in this order: `OC_PAGER_BIN`, then next to `oceancode` (also next to its target if `oceancode` is a symlink), then `PATH`. Here `--verbose` becomes the pager's `--debug`. | Flag | Type | Default | Meaning | |---|---|---|---| | `--listen` | string | `127.0.0.1:4096` | HTTP bind address handed to the pager. A non-loopback address needs `OC_SERVER_PASSWORD`. | | `--tui` | string | empty | Kept for old scripts: empty or `pager` is accepted, `ocean` is refused (the native TUI was removed), and any other value is an error. Falls back to `OC_TUI`. | See [19 · Ocean pager](/oceancode/pager). ## `oceancode run` ```bash oceancode run [flags] <message…> ``` Runs one non-interactive turn and exits with the typed code above. **Flags must come before the message**, because a flag after it is sent to the model as text; a bound flag placed after the message also prints a warning that the run is not bounded by it. When stdin is not a terminal, `run` reads it to EOF and appends it to the message, so close stdin (`</dev/null`) when the prompt is only an argument. An empty message fails with `you must provide a message`. | Flag | Type | Default | Meaning | |---|---|---|---| | `--agent` | string | empty | Agent to use. | | `--model`, `-m` | string | empty | Model to use, `provider/model`. | | `--variant` | string | empty | Model variant (provider-specific reasoning effort, e.g. `high`, `max`). | | `--title` | string | truncated prompt | Session title. | | `--continue`, `-c` | bool | off | Continue the last session. | | `--session`, `-s` | string | empty | Session id to continue. | | `--resume-title`, `-r` | string | empty | Resume the most recent session whose title matches this text. | | `--fork` | bool | off | Fork the session before continuing. An error unless `--continue` or `--session` is also given. | | `--auto` | bool | off | Auto-approve permissions that are not explicitly denied (dangerous!). | | `--output-format` | string | `text` | `stream-json` (NDJSON per event), `json` (one buffered result object), or `text`. Takes precedence over `--format`. | | `--format` | string | `default` | Deprecated. `default` = text; `json` = the `stream-json` framing. | | `--tools` | string | all | Restrict the advertised tools to these comma-separated ids. An unknown id is an error. | | `--disallowed-tools` | string | empty | Remove these comma-separated tool ids from the advertised set. | | `--allow` | string | empty | Append allow permission rules, e.g. `"Bash(git *),Edit(src/**)"`. | | `--deny` | string | empty | Append deny permission rules (last match wins, so a `--deny` beats an overlapping `--allow`). | | `--append-system-prompt` | string | empty | Append this text to the run's system context. | | `--json-schema` | string | empty | Validate the model's final JSON against this JSON Schema (a file path or inline JSON). On a mismatch the model is re-prompted with the validation errors, up to a retry limit. Prints the validated object; exits 6 if the schema cannot be satisfied. | | `--max-usd` | string | unset | USD bound for this session: a number, `off`, or `subscription` (no bound; spend shows the list-price estimate). | | `--max-minutes` | string | unset | Wall-clock bound: minutes, `90m`, `8h`, `2d`, or `off`. | | `--max-steps` | string | unset | Step bound per turn, `1..=1000`, or `default` to clear a stored override. | | `--tideline`, `--shelve` | string | unset | The Session Ledger tideline: `<pct>%` (a share of the model's context), `<limit>`, or `default`. `--shelve` is the older spelling. | | `--window` | string | — | Removed. Any value fails with `--window was renamed: use --tideline`. | | `--subagent` | string | unset | The model this session's sub-agents run, `provider/model`. | | `--atif` | string | empty | When the turn ends, write the session's ATIF-v1.8 trajectory (JSON) to this path. | See [07 · Headless and server](/oceancode/headless-and-server) and [08 · Bounding a long run](/oceancode/bounding-a-long-run). ## `oceancode serve` ```bash oceancode serve [--port <n>] [--host <ip>] [--db <path>] [--directory <dir>] [-v] ``` Starts the engine with its HTTP surface only and no UI. The banner goes to stdout and diagnostics to stderr. A non-loopback `--host` without `OC_SERVER_PASSWORD` is refused before anything boots. | Flag | Type | Default | Meaning | |---|---|---|---| | `--port` | int | `4096` | TCP port to listen on. | | `--host` | string | `127.0.0.1` | Interface to bind. A non-loopback host requires `OC_SERVER_PASSWORD`. | See [07 · Headless and server](/oceancode/headless-and-server). ## `oceancode acp` ```bash oceancode acp [--db <path>] [--directory <dir>] [-v] ``` Serves the engine as an Agent Client Protocol agent over stdin/stdout, which is how Xcode and other ACP editors launch it. stdout carries only newline-delimited JSON-RPC; the banner, diagnostics and tracing go to stderr. There is no HTTP surface, and the command returns when the editor closes stdin. It takes only the common engine flags. See [20 · Xcode and other ACP editors](/oceancode/xcode). ## `oceancode workflow` ```bash oceancode workflow run <name|path> [--args <json>] [--budget <n>] [--validate-only] [-m <provider/model>] oceancode workflow runs [--all] [--directory <dir>] oceancode workflow pause|resume|stop <run_id> [--budget <n>] ``` `run` starts a host-run Rhai workflow and prints each `workflow_updated` body to stdout as one JSON line. The last line is the terminating body, and the command exits with the workflow/goal code above. Put the name or path **first**, then the flags. `runs` lists the persisted runs for the current project, newest first; it takes no `--db` or `--verbose`. `pause`, `resume` and `stop` (run id first) only act on a run that is live in the engine that same command boots. | Flag | Sub-subcommands | Type | Default | Meaning | |---|---|---|---|---| | `--args` | `run` | string | `{}` | JSON object bound to the workflow's `args` global. | | `--budget` | `run`, control verbs | string | from config | `run`: absolute bound on cumulative child-agent calls, `1..=1024`. `resume`: a higher call bound that lifts a budget-limited run. | | `--validate-only` | `run` | bool | off | Metadata and compile check only; launches no agent. | | `--model`, `-m` | `run` | string | empty | `provider/model` for steps that name no model. There is no default provider. | | `--all` | `runs` | bool | off | List runs from every project. | See [07 · Headless and server](/oceancode/headless-and-server). ## `oceancode goal` ```bash oceancode goal "<objective>" [--budget <n>] [-m <provider/model>] [--db <path>] [--directory <dir>] [-v] ``` Launches the built-in `goal` workflow (Plan → Work → Verify) and prints each `goal_updated` body to stdout as one JSON line. It exits with the workflow/goal code above. The **objective must be the first argument**, with the flags after it. A missing objective, or a flag in first place, fails with `missing objective: usage: oceancode goal "<objective>" [--budget <n>] [--model <provider/model>]`. | Flag | Type | Default | Meaning | |---|---|---|---| | `--budget` | string | from config | Absolute bound on cumulative child-agent calls for the goal, `1..=1024`. | | `--model`, `-m` | string | empty | Default model for every goal agent that names none. There is no default provider. | See [07 · Headless and server](/oceancode/headless-and-server). ## `oceancode ledger` ```bash oceancode ledger path [--db <path>] [--directory <dir>] <session-id> oceancode ledger rebuild [--merge] [--yes] [--db <path>] [--directory <dir>] <session-id> ``` `path` prints the absolute ledger directory of the session. `rebuild` rebuilds the ledger mechanically and prints `rebuilt <n> turn(s) in <dir>`. `--merge` then prints the number of merge chunks and asks `fold forward with model calls? [y/N]` on stderr before folding them forward, which costs model calls. The session id is read after the flags, so **write the flags first**; a flag placed after the id is not applied. With the ledger disabled (`ledger.enabled = false`), `rebuild` fails. | Flag | Type | Default | Meaning | |---|---|---|---| | `--merge` | bool | off | `rebuild` only: fold the merge backlog forward after the rebuild (model calls). | | `--yes` | bool | off | `rebuild` only: skip the y/N confirmation for `--merge`. | See [06 · Session ledger and export](/oceancode/session-ledger-and-export). ## `oceancode export` ```bash oceancode export (--md | --atif) [--out <path>|-] [--db <path>] [--directory <dir>] <session-id> ``` Writes a whole session as one Markdown document or as an ATIF-v1.8 trajectory (JSON). Pass exactly one of `--md` or `--atif`. As with `ledger`, the session id is read after the flags, so write the flags first. | Flag | Type | Default | Meaning | |---|---|---|---| | `--md` | bool | off | Write the session as one Markdown document. | | `--atif` | bool | off | Write the session as an ATIF-v1.8 trajectory (JSON). | | `--out` | string | `-` | Output path; `-` writes to stdout. | See [06 · Session ledger and export](/oceancode/session-ledger-and-export). ## `oceancode login` ```bash oceancode login [--provider claude|copilot|codex] [--no-browser] ``` Logs in to a provider and stores the credential in the data dir's `auth.json`. `claude` (the default; also `anthropic`, `claude-pro`, `claude-max`) runs the Claude Pro/Max OAuth flow: it prints the authorize URL and reads the pasted code from stdin. `copilot` (`github-copilot`) and `codex` (`openai`) run the OAuth 2.0 device-code flow and store the credential under `github-copilot` or `openai`. | Flag | Type | Default | Meaning | |---|---|---|---| | `--provider` | string | `claude` | Provider to log in to: `claude`, `copilot` or `codex`. | | `--no-browser` | bool | off | Print the authorize URL only; do not try to open a browser. | A `--features platform` build adds `--provider ocean` (dashboard sign-in that stores the Ocean account key) and `--sso-token <token>` (stores a gateway SSO session token). See [15 · Platform mode](/oceancode/platform-mode). ## `oceancode logout` ```bash oceancode logout [anthropic|zai-coding-plan|ocean] ``` Removes one provider's stored credential from `auth.json`; the default is `anthropic`. Accepted names: `anthropic`/`claude`, `zai-coding-plan`/`zai`/`glm`/`zai-coding`/`glmcoding`, and `ocean`/`plungeai`. If nothing is stored, it prints `No stored <provider> credential.` and exits 0. Takes no flags. ## `oceancode key` ```bash oceancode key [<provider>] [<api-key>] ``` Stores an API key in `auth.json` (type `api`). The provider defaults to `anthropic` and takes the same names as `logout`. With no key on the command line, the key is read from stdin when stdin or stdout is not a terminal; in a terminal, a non-Anthropic provider prompts for it. With no key at all, the command prints the credential status for `anthropic` and `zai-coding-plan`. A key without the expected `sk-ant-` prefix triggers a warning but is still stored. Takes no flags. ## `oceancode auth` ```bash oceancode auth ``` An interactive picker: `1` Anthropic API key, `2` Claude subscription (runs `login`), `3` GLM coding plan (runs `key zai-coding-plan`). Needs both stdin and stdout to be a terminal and fails otherwise. Takes no flags. ## `oceancode claude` ```bash oceancode claude ``` Checks the claude-code provider before you use it. It reports the `claude` binary's path (or `claude.bin` from config), its `--version`, and whether a Claude Code login is present. Advisory only: it exits 0 whatever it finds. Any argument fails with `usage: oceancode claude`. ## `oceancode inspect` ```bash oceancode inspect [--json] ``` Prints where every config key, skill, agent, command, MCP server and instruction file comes from, for the **current directory** (there are no `--dir`/`--home` flags). Each human row is `kind id winning-layer source`, and the last line is `INSPECT_OK sources=<n>`. With `--json` the JSON array goes to stdout and the sentinel to stderr. Boots nothing. See [09 · Capabilities and plugins](/oceancode/capabilities). ## `oceancode plugin` ```bash oceancode plugin [--root <home>] <subcommand> ``` `--root` (also `--root=<home>`) overrides the home that the stores live under (`<home>/.oceancode/plugins` and `<home>/.oceancode/marketplaces`). | Sub-subcommand | Prints | |---|---| | `marketplace add <spec>` / `update <n>` / `remove <n>` | `PLUGIN marketplace-added\|marketplace-updated\|marketplace-removed <name>` | | `marketplace list` | `<name> plugins=<n>` per marketplace | | `install <name-or-path> [--trust] [--marketplace <m>]` | `PLUGIN installed <name> <version>`, plus `PLUGIN trusted <name>` with `--trust` | | `list` | `<name> <version> enabled=<bool> trusted=<bool>` per plugin | | `uninstall <n>` / `update <n>` | `PLUGIN uninstalled <n>` / `PLUGIN updated <n> <version>` | | `enable <n>` / `disable <n>` | `PLUGIN enabled\|disabled <n>` | | `enable-all` / `disable-all` | `PLUGIN enabled-all\|disabled-all count=<n>` | | `trust <n> [--off]` | `PLUGIN trusted\|untrusted <n>` | | `validate <n>` | `PLUGIN validated <n> ok=<bool>`, then one ` - <problem>` line per problem | | `import --from claude-code [--trust] [--no-repo-trust] [--dry-run] [--home <h>] [--dir <d>]` | Ends with `IMPORT_OK plugins=<n> marketplaces=<m> repo_trust=<bool>` (`IMPORT_DRY_RUN_OK …` under `--dry-run`). `import` takes `--home`/`--dir`, not `--root`. | See [09 · Capabilities and plugins](/oceancode/capabilities). ## `oceancode mcp` ```bash oceancode mcp [--root <dir> | --dir <dir>] [--home <h>] <subcommand> ``` `--root` (or its alias `--dir`) selects the project directory where `.mcp.json` lives; the default is the current directory. `--home` selects the home tier. | Sub-subcommand | Behaviour | |---|---| | `add <name> --command <cmd> [--arg <a>]… [--env K=V]…` | Writes a stdio server to `<dir>/.mcp.json` under `mcpServers.<name>`. Prints `MCP added <name> stdio`. | | `add <name> --url <url> [--header K=V]…` | Writes a remote (`type: http`) server. Prints `MCP added <name> remote`. Pass exactly one of `--command` or `--url`. | | `remove <name>` | Deletes the entry from `.mcp.json`. Prints `MCP removed <name>`. | | `enable <name>` / `disable <name>` | Clears or sets the server's held-disabled bit in the trust store. Prints `MCP_ENABLED`/`MCP_DISABLED <name>`. | | `list` | One row per discovered server: `name stdio\|remote tier trusted\|untrusted enabled\|disabled\|shadowed source`. | | `login <name>` / `logout <name>` | `login` prints the OAuth callback URL and contacts no network. `logout` clears the stored OAuth entry. | | `doctor` | Connects to each server with a 2-second budget and prints one status row per server (`connected`, `needs-auth`, `failed`, `disabled`, `untrusted`). | See [11 · MCP servers](/oceancode/mcp). ## `oceancode skill` ```bash oceancode skill [--root <dir>] [--dir <d>] [--home <h>] <subcommand> ``` | Sub-subcommand | Behaviour | |---|---| | `create <name> [--description <text>]` | Writes `<root>/.oceancode/skills/<name>/SKILL.md` (`--root` defaults to the current directory) and prints `SKILL created <path>`. Refuses to overwrite an existing skill. | | `list [--json]` | One row per discovered skill: `id tier trusted\|untrusted state description`. `state` is `enabled` or the reason the skill is disabled. | | `enable <id>` / `disable <id>` | Flips one skill's user-tier enable bit. `<id>` is a stable id, or a bare name that matches exactly one skill. Prints `SKILL enabled\|disabled <id>`. | | `enable-all` / `disable-all` | Prints `SKILL enabled-all` / `SKILL disabled-all count=<n>`. | See [09 · Capabilities and plugins](/oceancode/capabilities). ## `oceancode trust` ```bash oceancode trust show | repo on|off | mcp <name> on|off|clear [--dir <d>] [--home <h>] ``` Reads and sets the per-project trust record in `<home>/.oceancode/trust.json`. `show` prints `repo=<bool>`, then one `mcp <name>=<bool>` line per override and one `disabled <name>` line per held server. Every subcommand ends with `TRUST_OK`. `repo` grants or revokes repo-tier execution (repo hooks and `.mcp.json` servers). `mcp` sets a per-server override that takes precedence over `repo`, or clears it. See [11 · MCP servers](/oceancode/mcp). ## `oceancode hooks` ```bash oceancode hooks [--dir <d>] [--home <h>] list [--json] oceancode hooks add <Event> --command <cmd> [--matcher <re>] [--timeout <s>] [--scope user|project|local] oceancode hooks remove <Event> --command <cmd> [--matcher <re>] [--scope user|project|local] ``` `list` prints every hook source file the boot loader reads: one line per source with its tier, its trust, whether it is loaded (or held, and why) and its path, followed by its handlers. The output ends with `HOOKS_OK sources=<n> handlers=<m> loaded=<k>`. `--json` prints `{"repo_trusted", "sources"}` and no sentinel. `add` and `remove` edit Claude Code's `settings.json` for the chosen scope (default `project`) and print `HOOK_ADDED <path>` or `HOOK_REMOVED <path>`. Removing a handler that does not exist fails with `hooks remove: no such handler`. See [10 · Hooks](/oceancode/hooks). ## `oceancode jev` ```bash oceancode jev status [--json] oceancode jev probe [--record --point <p> --case <id> --request <req.json> --out <fixture.json> [--expect <json>]] oceancode jev labels [--implicit | --unlabelled [--point <p>]] [--root <ledger dir>] [--out <file>] oceancode jev questions --json oceancode jev verdicts --in <cases.jsonl> ``` Inspects the Jev decision layer, read from the current directory and the environment (no `--dir`/`--home` flags). `status` prints one `point mode endpoint-host` line for each of the nine decision points — `permission`, `output_view`, `supervisor`, `triage`, `visibility`, `check`, `guidance`, `snippets`, `effort` — then `JEV_STATUS_OK points=9`; `--json` prints `{points, endpoint, thresholds}` instead, with `JEV_STATUS_EMITTED points=9` on stderr. On a project with no `jev` section every point is `off`: ```text permission off - output_view off - … effort off - JEV_STATUS_OK points=9 ``` `probe` makes one live call to Jev and prints `JEV_PROBE_OK` or `JEV_PROBE_FAIL <why>`; `probe --record` records a fixture. `labels` exports labelled decisions (`JEV_LABELS_OK added=<n> total=<m>`), each row's `source` telling a person's answer (`shadow-user`) from the labels the `effort` and `snippets` points write themselves (`implicit-effort`, `implicit-snippets`); `--unlabelled` requires `--out`. The `tool_tiers` and `relevance` points were retired on 2026-10-21 and no verb lists them. `questions --json` and `verdicts --in` print the decision points' question sets and policy verdicts for the bench. Only `probe` calls Jev. See [13 · Jev decisions](/oceancode/jev-decisions). ## `oceancode doctor` ```bash oceancode doctor [--storage] ``` Reports orphaned sessions (registry entries whose process died without a clean exit) and captured panic reports found under the data dir. `--storage` adds the storage report: the sizes of `oceancode.db` and its WAL, the session count, and the ledger directory's size and oldest session age. Read-only; any other argument is ignored. See [18 · Operations](/oceancode/operations). ## `oceancode migrate` ```bash oceancode migrate [--db <path>] ``` Opens the database named by `--db` (default `oceancode.db`), migrates it to the current schema, prints `opened and migrated <path>`, and exits. ## `oceancode update` ```bash oceancode update [--self-test] ``` Updates itself to the newest release on the configured channel; `OC_UPDATE_CHANNEL` (`stable`/`beta`) overrides `update.channel`. Both `oceancode` and `ocean-pager` are verified first, swapped atomically, and rolled back together if a health check fails. Prints `UPDATE_APPLIED from=<old> to=<new>`, or `oceancode <v> is already the newest <channel> release`. `--self-test` exercises the swap and rollback offline on stub binaries and prints `UPDATE_OK from=<old> to=<new> rollback_tested=1`, then `UPDATE_SELF_TEST_OK binaries=2`. No other argument is recognised. See [17 · Distribution and updates](/oceancode/distribution). ## `oceancode verify-release` ```bash oceancode verify-release <artifact> [--pubkey <base64>] [--sha256sums <path>] ``` Verifies the artifact's minisign `<artifact>.minisig` signature without any external tool. `--pubkey` replaces the embedded release key; `--sha256sums` also checks the artifact's entry in that `SHA256SUMS` file. Prints `VERIFY_RELEASE_OK file=<name>`. See [17 · Distribution and updates](/oceancode/distribution). ## `oceancode uninstall` ```bash oceancode uninstall [--purge] [--yes] [--dry-run] ``` Removes the installed `oceancode` binary from `OC_INSTALL_DIR` (default `~/.local/bin`). `--purge` also removes the app's config and data dirs, but never a project's `.oceancode/oceancode.db`. Unless `--yes` is given, it lists every path and asks you to type `oceancode` to confirm. `--dry-run` prints `would remove: <path>` lines and removes nothing. The last line is `UNINSTALL_OK removed=<n>`. See [17 · Distribution and updates](/oceancode/distribution). ## See also - [01 · Getting started](/oceancode) — the three boot shapes. - [02 · Configuration](/oceancode/configuration) — every `OC_*` variable these commands read. - [07 · Headless and server](/oceancode/headless-and-server) — `run`, `serve`, `workflow`, `goal` in depth. - [09 · Capabilities and plugins](/oceancode/capabilities), [10 · Hooks](/oceancode/hooks), [11 · MCP servers](/oceancode/mcp) — the capability commands. - [17 · Distribution and updates](/oceancode/distribution) — `update`, `verify-release`, `uninstall`. # Ocean pager reference Source: https://www.plungeai.com/docs/oceancode/pager-reference This page lists Ocean pager's slash commands, keyboard shortcuts, mode ring, `config.toml` settings, themes and notification options. For how the pager starts, its environment variables and what is wired to the Ocean engine, see [Ocean pager](/oceancode/pager). ## Slash commands Type `/` in the composer to open the menu. A prefix narrows the list, and `Tab` or `Enter` inserts the command. Enabled skills appear in the same menu ([19 · Ocean pager](/oceancode/pager#what-is-wired-to-the-ocean-engine)). In the **Needs** column, *session* means an active session, *fullscreen* or *minimal* means that screen mode only (in the other mode the command tells you to switch), and *dashboard* means the Agent Dashboard. *args* means the command needs an argument. ### Session and conversation | Command | Aliases | What it does | Needs | |---|---|---|---| | `/new` | `/clear` | Start a new session | — | | `/resume` | — | Resume a previous session | — | | `/fork [--worktree\|--no-worktree] [directive]` | — | Branch the current session into a peer agent | session | | `/rename <title> \| --auto` | `/title` | Rename the current session; `--auto` returns to the automatic title | session, args | | `/delete` | — | Delete this session | session | | `/session-info` | — | Show session info | session | | `/btw <question>` | — | Ask a side question without interrupting the turn | session, args | | `/recap` | `/summarize` | Summarize the session so far; listed only when session recap is available | session | | `/rewind` | `/undo` | Rewind the conversation, and the files the file tools changed, to a previous turn | session | | `/redo` | — | Re-apply the turn a rewind reverted, files included (`ocean/restore_code`) | session | | `/compact [instructions]` | — | Compact the conversation history (`ocean/compact_conversation`) | session | | `/queue` | — | List the prompts queued behind the running turn | session | | `/history` | — | Search prompt history | session | | `/loop [interval] <prompt>` | — | Run a prompt on a recurring interval | args | | `/plan [description]` | — | Enter plan mode; `add`/`done`/`cancel`/`edit`/`remove` edit the live todo table (`_ocean/todos/set`) | session | | `/view-plan` | `/show-plan`, `/plan-view` | View the current plan | session | | `/share` | — | Share this session via a public URL (`ocean/share_session`); hidden from the menu until enabled, but you can still type it | session | | `/unshare` | — | Revoke this session's share link (`ocean/unshare_session`) | session | ### Ocean engine controls These commands use `_ocean/*` methods. For their argument forms, see [19 · Ocean session commands](/oceancode/pager#ocean-session-commands). | Command | Aliases | What it does | Needs | |---|---|---|---| | `/tideline [now \| <n>% \| <tokens> \| default]` | `/shelve` (hidden) | Show or set the Session Ledger tideline (`_ocean/ledger/status`) | session | | `/retention [<days> \| off]` | — | Show or set how many days idle sessions are kept — one setting for the database and the ledger, written to the global config (`_ocean/retention`) | session | | `/budget [<usd> \| off \| subscription]` | — | Show or set this session's spend bound (`_ocean/session/bounds`) | session | | `/limits [steps <n\|default> \| time <dur\|off>]` | — | Show or set the per-turn step and time bounds (`_ocean/session/bounds`) | session | | `/subagent [<provider/model> \| default \| off \| clear]` | — | Show or set the sub-agent model (`_ocean/subagent_model`) | session | | `/mode [<id> \| default]` | — | Bare opens the work-mode picker (as `F4`); `<id>` sets this session's work mode, `default` clears it (`_ocean/mode`, [23](/oceancode/work-modes)) | session | ### Model, context and output | Command | Aliases | What it does | Needs | |---|---|---|---| | `/model <name> [effort]` | `/m` | Switch the active model | session, args | | `/effort <level>` | — | Set reasoning effort for the current model | session, args | | `/connect <provider>` | — | Connect a model provider (API key or account login) | args | | `/context` | — | View context usage | session | | `/usage` | `/cost` | View usage | — | | `/copy [N] [file]` | — | Copy the last response (or the Nth) to the clipboard or a file | session | | `/export [filename]` | — | Export the current conversation to a file or the clipboard | session | | `/transcript` | `/log` | View the conversation transcript in `$PAGER` | session | | `/find [text]` | — | Search the conversation scrollback | session, fullscreen | | `/jump` | — | Jump to a turn in the conversation | session, fullscreen | | `/expand` | — | Re-print the last collapsed block, fully expanded | session, minimal | | `/edit-prompt` | — | Open an external editor for an empty prompt (to keep a draft, use the command palette) | session | ### Extensions and workflows | Command | Aliases | What it does | Needs | |---|---|---|---| | `/skills` | — | View skills | — | | `/plugins` | `/plugin` | View plugins | — | | `/hooks` | — | View hooks | — | | `/mcps` | — | Show MCP server status | — | | `/marketplace` | — | View the marketplace | — | | `/workflows` | — | Browse installed workflows | — | | `/workflow` | — | Launch a saved workflow, list runs, or manage a run (pause, resume, stop, save) | session | ### Settings and display | Command | Aliases | What it does | Needs | |---|---|---|---| | `/settings` | `/config`, `/preferences`, `/prefs` | Open the settings modal | — | | `/theme [name]` | `/t` | Switch the color theme | fullscreen | | `/auto` | — | Toggle auto mode (a classifier approves safe tools); listed only when auto mode is available | — | | `/always-approve` | — | Toggle always-approve (skips all permission prompts) | — | | `/vim-mode` | — | Toggle vim-style scrollback keys | — | | `/multiline` | `/ml` | Toggle multiline input (swaps `Enter` and `Shift+Enter`) | — | | `/compact-mode` | — | Toggle compact UI (less padding) | — | | `/timestamps` | — | Toggle message timestamps | — | | `/timeline` | — | Toggle the timeline sidebar | fullscreen | | `/notify [on \| off]` | — | Toggle desktop notifications ([Notifications](#notifications)) | — | | `/toggle-mouse-reporting` | — | Toggle terminal mouse reporting, so the terminal's own click-drag copy works | — | | `/minimal` | — | Switch this session to minimal (scrollback-native) mode | session, fullscreen | | `/fullscreen` | `/full` | Switch this session to fullscreen mode | session, minimal | ### Navigation, help and maintenance | Command | Aliases | What it does | Needs | |---|---|---|---| | `/dashboard` | `/agents-dashboard`, `/sessions` | Open the Agent Dashboard; listed only when the dashboard is enabled | fullscreen | | `/cd [path]` | — | Change the working directory for new agents | dashboard | | `/home` | `/welcome` | Return to the welcome screen | — | | `/help` | — | Browse commands and keyboard shortcuts | — | | `/docs [web\|title]` | `/howto`, `/guides` | Open the in-TUI how-to guides, or (`web`) the published developer guide, `https://code.plungeai.com/docs`, in the browser | — | | `/release-notes` | `/changelog` | View release notes for the current version | — | | `/doctor [fix [FIX]]` | `/terminal-setup`, `/terminal-check`, `/terminal-info` | Check this session and show the fixes available | session | | `/quit` | `/exit` | Quit Ocean Code (asks first) | — | | `/debug [scroll\|fps\|log]` | — | Toggle debug overlays; listed only on debug builds | — | | `/scroll-debug` | — | Toggle the scroll-diagnostics HUD; never listed | — | ## Keyboard The defaults live in a single registry. Keys apply where the **Where** column says: *prompt* when the composer has focus, *scrollback* when the conversation has focus (`Tab` switches between them), *agent* anywhere on the agent screen, and *global* everywhere. `Ctrl+.` (or `Ctrl+X`) opens the full cheatsheet. ### Essentials | Keys | Where | Action | |---|---|---| | `Enter` | prompt | Send | | `Ctrl+Enter` (alt `Ctrl+I`) | prompt | Send now while a turn runs: cancels the current turn and sends the message next. In Apple Terminal it is `Ctrl+O`; in VS Code-family terminals, `Ctrl+L` | | `Tab` | prompt / scrollback | Focus the scrollback / focus the prompt (`i` or `Space` also focus the prompt) | | `Ctrl+C` | agent | Cancel the turn | | `Shift+Tab` | prompt | Cycle the mode ring ([Modes](#modes-shifttab-ring)) | | `Option+Tab` / `F3` | prompt | Cycle reasoning effort for this session | | `F4` | agent | Open the work-mode picker ([Work-mode picker](#work-mode-picker)) | | `Ctrl+O` | agent | Toggle always-approve | | `Ctrl+P` (alt `?`) | agent | Command palette: search every action and slash command by name | | `Ctrl+.` (alt `Ctrl+X`) | agent | Keyboard shortcuts. Where `Ctrl+.` is unreliable (Windows, WSL, some terminals) the two keys swap | | `Ctrl+M` | agent | Pick a model (when the prompt has focus, `Ctrl+M` toggles multiline instead) | | `F2` (alt `Ctrl+,`, `Cmd+,`) | agent | Open the settings modal | | `Ctrl+N` | global | New session | | `Ctrl+Q` (alt `Ctrl+D`) | global | Quit; press twice in quick succession. In VS Code's terminal, `Ctrl+D` only | ### Prompt | Keys | Where | Action | |---|---|---| | `Ctrl+M` | prompt | Toggle multiline | | `Ctrl+S` (alt `Alt+S`) | prompt | Stash the draft; on an empty composer, pop it back. A new stash replaces the old one | | `!` | prompt | Shell mode (type it on an empty prompt) | | `Up` | prompt | Browse prompt history (empty prompt). `/history` opens the search panel | | `Esc` `Esc` | prompt | With text and no turn running: clear the prompt (`Ctrl+Z` restores it) | | `Esc` `Esc` | prompt / scrollback | With an empty prompt and at least one turn: open the rewind picker | | `Esc` | while a turn runs | Never cancels; a toast names the cancel key | The double-press window is 800 ms (`OC_PAGER_ESC_DOUBLE_PRESS_MS` overrides it). ### Panes and sessions | Keys | Where | Action | |---|---|---| | `Ctrl+T` | agent | Toggle the todo pane | | `Ctrl+G` | agent | Hide or show the dock above the prompt (the tasks pane at the top with `OC_PAGER_DOCK=0`). In minimal mode, edit the prompt in `$VISUAL` / `$EDITOR` (`vi` if neither is set) | | `Ctrl+B` | agent | Send the running task to the background | | `Ctrl+;` (alt `Ctrl+'`) | agent | Toggle the prompt queue. In VS Code-family terminals on a local Mac, `Ctrl+4` | | `Ctrl+R` | agent | Open sessions | | `Ctrl+L` | agent | Open extensions (unbound in VS Code-family terminals; use `/plugins`) | | `Ctrl+\` (alt `Ctrl+4`) | global | Open the Agent Dashboard | ### Scrollback Bare-letter keys in this table work only when [`vim_mode`](#settings-configtoml) is on (`/vim-mode`). Arrows, `PgUp`/`PgDn`, `Enter`, `Tab`, `?` and every `Ctrl` chord always work. | Keys | Action | |---|---| | `j` / `k` (`Down` / `Up`) | Select the next / previous entry | | `L` / `H` (`Shift+Right` / `Shift+Left`) | Next / previous turn | | `J` / `K` | Jump to the next / previous turn at the top of the viewport | | `g` / `G` | Go to the top / bottom | | `Ctrl+K` / `Ctrl+J` | Scroll up / down one line | | `Ctrl+U` / `Ctrl+D` | Scroll up / down half a page (in VS Code, `D` scrolls down) | | `PgUp` / `PgDn` | Scroll one page | | `h` / `l` (`Left` / `Right`) | Collapse / expand the selected entry | | `e` / `E` | Expand or collapse the entry / every entry | | `Ctrl+E` | Toggle all thinking blocks | | `r` | Toggle raw markdown | | `y` / `Y` | Copy the content / the command or path | | `Enter` (alt `Ctrl+F`) | Open the entry in the viewer | | `o` / `O` | Next / previous link | | `x` | Kill the selected background task | | `Ctrl+R` | Toggle mouse reporting, only when `[ui] mouse_reporting_toggle` is on (it then replaces Open sessions while the scrollback has focus) | ### Agent Dashboard | Keys | Action | |---|---| | `Down` / `Up` (`j` / `k` with vim mode) | Select the next / previous row | | `Shift+Down` / `Shift+Up` | Move the agent down / up | | `Shift+Tab` | Cycle the dispatch mode | | `Ctrl+T` | Pin / unpin the agent | | `Ctrl+R` | Rename the agent | | `Ctrl+X` | Stop / delete the agent | | `Ctrl+O` | Toggle always-approve for the selected agent | | `Ctrl+G` | Toggle row grouping | | `Ctrl+L` | Change the working directory for new agents | | `Ctrl+W` | Toggle worktree mode for new agents | | `Ctrl+.` (or `?`) | Show the shortcuts overlay | | `Esc` | Close the dashboard | | `Ctrl+\` | (inside an attached session) Back to the dashboard | | `Ctrl+[` / `Ctrl+]` | (inside an attached session) Previous / next session | | `Ctrl+X` | (inside an attached session) Stop the agent and go back to the dashboard | ### Mouse Click a pane to focus it. The wheel and trackpad scroll the pane (see the `scroll_*` settings), and you can click or drag the scrollbar. In-app selection and double-click follow [`keep_text_selection`](#settings-configtoml); hold `Shift` while dragging to use the terminal's own selection. Queued prompts have `[Interject]`, `[edit]` and `[cancel]` buttons. Double-clicking a `[Pasted: N lines]` chip expands it into editable text. ### Work-mode picker `F4` or a bare `/mode` opens it in the prompt's place: the modes on the left (Model first, tagged `default`; custom diagrams after a `── custom ──` divider; on a narrow terminal the labels only) with the highlighted mode's description under them, cut with `…` when it runs out of rows, and its picture and cost on the right. The footer names the keys, `e: copy` too when the width allows ([23 · Work modes](/oceancode/work-modes)). | Keys | Action | |---|---| | `Down` / `Up` (`j` / `k`) | Move; the picture follows at once | | `Enter` | Use the mode for this session and close | | `d` | Make it the current agent's default (`agents.<id>.mode` in the user `oceancode.json`) and close | | `e` | Copy the highlighted built-in to `.oceancode/modes/<id>-copy.jsonc` and close; the toast names the path | | `Esc` | Close without a change | Every mode but Model puts its badge on the prompt's top border, after the `ultracode` badge when that is on: `solo`, `delegate ≤4`, `pair`, `pipeline`, `orchestrate ≤8`, `goal`, `race ×3`, and for Auto `auto` until its first pick, then `auto → <pick> · <reason>`. Model has no badge. ## Modes (Shift+Tab ring) `Shift+Tab` cycles **Build → Auto → Plan → Ask → Debug → Build**. Build, Plan, Ask and Debug are the engine's agents; Auto is a permission stop that keeps the Build agent and sets the `auto` permission mode. The prompt border shows the current mode in its color: | Mode | Border | |---|---| | `build` | blue | | `auto` | yellow | | `plan` | violet | | `ask` | green | | `debug` | orange | | `always-approve` | red | The ring skips the Auto stop while always-approve is on, or when auto mode is not available. Leaving Auto writes `permission_mode = "ask"` back. What each permission mode allows is in [19 · Shift+Tab ring and permission modes](/oceancode/pager#shifttab-ring-and-permission-modes). ## Settings (`config.toml`) The settings modal (`/settings`, `F2`) edits these settings. Unless the **Key** column gives another table, each is written to the `[ui]` table of `<pager home>/config.toml` (`$OC_PAGER_HOME`). *Restart* means the change takes effect on the next start. Rows in the modal's *Hidden in minimal* set still apply to the full TUI. ### Appearance | Key | Type | Default | Meaning | |---|---|---|---| | `theme` | enum | `oceannight` | Color theme ([Themes](#themes)) | | `auto_dark_theme` | enum | `oceannight` | Theme used in system dark mode (only with `theme = "auto"`) | | `auto_light_theme` | enum | `oceanday` | Theme used in system light mode (only with `theme = "auto"`) | | `screen_mode` | `fullscreen` \| `minimal` | `fullscreen` | How plain `ocean-pager` opens next time. Restart. `/minimal` and `/fullscreen` switch only the current session | | `compact_mode` | bool | `false` | Less padding around messages. Turns on automatically at 20 terminal rows or fewer | | `show_timestamps` | bool | `true` | Clock time next to user messages and agent responses | | `show_timeline` | bool | `false` | A per-turn tick rail in place of the scrollbar; hover to preview, click to jump | | `page_flip_on_send` | bool | `true` | Scroll a sent prompt to the top of the screen so the response starts on a fresh page | | `simple_mode` | bool | `true` | Plain readline-style input in the prompt instead of vim keys (vim input is experimental) | | `vim_mode` | bool | `false` | Vim keys for navigating the scrollback; the prompt is unaffected | | `render_mermaid` | `auto` \| `on` \| `off` | `auto` | `auto`/`on` show a clickable row that opens the rendered diagram; `off` shows the raw source | | `max_thoughts_width` | int 40–500 | `120` | Column width of the thoughts panel | | `show_thinking_blocks` | bool | `true` | Show thinking blocks while streaming | | `group_tool_verbs` | bool | `true` | Fold consecutive read/search/list tool calls and subagent rows into one summary row | | `show_edit_details` | bool | `true` | Show each edit's diff inline under its header. `false` shows a one-line `+N/−M` diffstat and merges back-to-back edits to the same file; expand a row to see the diff. `OC_PAGER_SHOW_EDIT_DETAILS` overrides it | | `show_run_details` | bool | `true` | When a Run finishes, show the command and the first lines of its output with a `… +N lines` fold; `false` leaves finished runs as a header-only row. `OC_PAGER_SHOW_RUN_DETAILS` overrides it | | `collapsed_edit_blocks` | bool | (unset) | Legacy, not in the modal: read only while `show_edit_details` is unset, as its inverse (`collapsed_edit_blocks = true` ≡ `show_edit_details = false`) | | `[ui.display_refresh] auto_cadence_enabled` | bool | `true` | Match the display's refresh rate when streaming and scrolling; off keeps about 60 Hz. Restart | | `respect_manual_folds` | bool | `false` | Keep manually folded blocks as they are while streaming (experimental). Written to `pager.toml` under `[scrollback.scroll]`, not `config.toml` | ### Mouse | Key | Type | Default | Meaning | |---|---|---|---| | `scroll_speed` | int 1–100 | `50` | Wheel and trackpad speed multiplier | | `scroll_mode` | `auto` \| `wheel` \| `trackpad` | `auto` | Force wheel or trackpad behavior when detection misreads your device | | `scroll_lines` | int 1–10 | `3` | Lines per scroll tick; until you set it, each terminal's own profile applies | | `invert_scroll` | bool | `false` | Reverse the vertical scroll direction | | `keep_text_selection` | `flash` \| `hold` \| `word_select` | `flash` | `flash` clears the selection after mouse-up, `hold` keeps it until `Esc`, a click or a scroll, and `word_select` makes a double-click select a word and a triple-click a paragraph | ### Editor, agent and models | Key | Type | Default | Meaning | |---|---|---|---| | `combine_queued_prompts` | bool | `false` | Merge consecutive plain follow-ups into one model turn | | `confirm_before_rewind` | bool | `true` | Ask before rewinding code and conversation | | `permission_mode` | `default` \| `ask` \| `auto` \| `always-approve` | `ask` | The permission mode; the `Shift+Tab` ring and `Ctrl+O` write it too | | `remember_tool_approvals` | bool | `true` | Offer "Always allow" options on permission prompts. Restart | | `default_selected_permission` | `always_allow_all_sessions` \| `allow_command_always` \| `allow_once` \| `reject` | `always_allow_all_sessions` | The option the cursor starts on in a permission prompt | | `[toolset.ask_user_question] timeout_enabled` | bool | `true` | The `ask_user_question` tool times out instead of waiting forever. Restart | | `[models] default` | model | (none) | The model for new sessions; changing it also switches the active session | | `fork_secondary_model` | model | (none) | The model for the secondary agent when you fork; clearing it restores the built-in default | | `hunk_tracker_mode` | `off` \| `agent_only` \| `all_dirty` | `off` | Which file changes are tracked as hunks; `off` also turns off line-count stats. Restart | | `[cli] show_tips` | bool | `true` | The tip-of-the-day banner on startup. Restart | | `[cli] auto_update` | bool | `true` | Download pager updates on startup. Restart. When `oceancode` launches the pager, auto-update is always off ([19](/oceancode/pager)) | **Agent defaults** (its own section, after Models) has two rows per ring agent: `<Agent> model` (a catalog model, or `(no override)`) and `<Agent> effort` (`inherit`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`). They are not pager settings: each change is an `_ocean/agents` call that writes `agents.<id>.model` / `agents.<id>.effort` into the user `oceancode.json` (`~/.config/oceancode/oceancode.json`, the rows fleet-router edits too), and `inherit` / `(no override)` write `""`. Shift+Tab to that agent applies them, the effort clamped to the levels the model offers ([19](/oceancode/pager#shifttab-ring-and-permission-modes), [02](/oceancode/configuration)). A write the engine refuses (an `oceancode.json` that does not parse, say) shows as `agent defaults not saved: …` and changes nothing. Each ring agent also has a work-`mode` row: a mode id, saved through `_ocean/mode` as `agents.<id>.mode` in the same user file ([23](/oceancode/work-modes)). **Sub-agents** (its own section) has three number rows, `at once`, `per turn` and `depth`: the sub-agent limits `subagent.max_concurrent` (`0`–`64`), `subagent.max_per_turn` (`1`–`1000`) and `subagent.max_depth` (`1`–`4`), written to the user `oceancode.json` through `_ocean/mode`. A set number wins over the work mode's own limit in every mode; an out-of-range one is refused with the engine's message ([23](/oceancode/work-modes#limits)). `[ui.contextual_hints]` holds the per-hint switches. All default to `true`, and the modal shows them as one *Show contextual hints* group: `undo` (`Ctrl+Z` restores a cleared prompt), `plan_mode` (suggests plan mode for a planning request), `image_input` (offers to paste a clipboard image), `send_now` (empty-prompt `Enter` sends the top queued item), `small_screen` (suggests `/compact-mode`), `word_select`, `export_copy` (points out `/copy` and `/export`) and `ssh_wrap` (a `/doctor` tip over SSH). Two modal rows are not saved. `multiline_mode` (default `false`) resets each session, and `plan_mode` (`off`/`on`, default `off`) is per session. ### Keys outside the modal | Key | Type | Default | Meaning | |---|---|---|---| | `[ui] require_plan_approval` | bool | `false` | The plan viewer always asks before leaving plan mode | | `[ui] mouse_reporting_toggle` | bool | `false` | Bind `Ctrl+R` in the scrollback to toggle mouse reporting (`OC_PAGER_MOUSE_REPORTING_TOGGLE` overrides it) | | `[cli] session_picker_grouped` | bool | on | Group the session picker | | `[dashboard] enabled` | bool | `true` | Enable `/dashboard` (`OC_PAGER_AGENT_DASHBOARD=0` turns it off) | ## Themes | Name | Display name | Notes | |---|---|---| | `oceannight` | Ocean Night | Default. A neutral dark theme with a magenta accent. Aliases `ocean-night`, `dark` | | `oceanday` | Ocean Day | A light theme. Aliases `ocean-day`, `light`, `day` | | `tokyonight` | Tokyo Night | Needs truecolor. Aliases `tokyo-night`, `tokyo` | | `rosepine-moon` | Rose Pine Moon | Needs truecolor. Aliases `rosepine`, `rose-pine`, `rose-pine-moon` | | `oscura-midnight` | Oscura Midnight | Needs truecolor. Alias `oscura` | | `terminal` | Terminal | Uses the terminal's own background and text colors. Offered only when the terminal theme is enabled (`OC_PAGER_TERMINAL_THEME`). Aliases `terminal-default`, `transparent`, `native` | | `auto` | Auto | Follows the system's dark/light appearance and picks `auto_dark_theme` or `auto_light_theme`. Alias `system` | Names are case-insensitive. On a terminal without truecolor, the themes that need it are not offered. The pre-rename keys `groknight`, `grok-night`, `grokday` and `grok-day` still load, for one release. `OC_PAGER_THEME` forces a theme without editing the config. ## Notifications Desktop notifications, the tab title and the progress indicator are set in `[ui.notifications]`: | Key | Default | Meaning | |---|---|---| | `method` | `auto` | `auto`, `osc9`, `osc99`, `osc777`, `bel` or `none` | | `condition` | `unfocused` | `unfocused` (only once the terminal has lost focus), `always` or `never` | | `idle_threshold_secs` | `3` | How many seconds the terminal must be unfocused before a notification fires | | `events` | `["turn_complete", "approval_required"]` | Also available: `session_ready`, `task_complete`, `agent_error` | | `sleep_prevention` | `true` | Keep the machine from idle-sleeping during a turn (macOS IOKit assertion, Linux `systemd-inhibit`) | | `progress_bar` | `true` | Show a progress indicator in the tab (OSC 9;4) | | `session_recap` | `true` | Show an automatic recap when you come back after being away | | `session_recap_threshold_secs` | `30` | How many seconds unfocused count as "away" for the automatic recap | | `[ui.notifications.title] enabled` | `true` | Set the terminal/tab title to reflect the agent's state | | `[ui.notifications.title] items` | `["action-required", "spinner", "activity", "session-name", "ocean"]` | Also available: `cwd`, `model`, `turn-timer` | **Protocol selection (`method = "auto"`).** The pager picks the protocol from the terminal: | Terminal | Protocol | |---|---| | iTerm2, WezTerm, Warp | OSC 9 | | Kitty | OSC 99 | | Ghostty, VTE, Terminator, foot | OSC 777 (titled "Ocean Code") | | Zellij (any terminal), and every other terminal | BEL | Under tmux the sequence is wrapped in DCS passthrough, so it reaches the outer terminal. Control characters are stripped from the text first. The progress indicator is drawn only on Ghostty, WezTerm and iTerm2 3.6 or later. **Custom commands.** Each `[[ui.notifications.hooks]]` entry runs `command` through `sh -c` for its `events`. The command gets `OC_PAGER_EVENT`, `OC_PAGER_MESSAGE` and `OC_PAGER_SESSION_ID`. `only_unfocused` defaults to `true`, and `timeout_secs` defaults to `10` (minimum 1); a hook that runs past its timeout is killed. ```toml [[ui.notifications.hooks]] command = "terminal-notifier -title 'Ocean Code' -message \"$OC_PAGER_MESSAGE\"" events = ["turn_complete", "approval_required"] ``` **`/notify`.** `/notify` toggles desktop notifications, and `/notify on` or `/notify off` sets them. Off sets `method` to `none` for the running pager. On restores the method you had before, or `auto`. It does not write `config.toml`. ## See also - [Ocean pager](/oceancode/pager): the binary, the environment variables, and what is wired to the engine - [Permissions and sandbox](/oceancode/permissions-and-sandbox): what each permission mode allows - [Session ledger and export](/oceancode/session-ledger-and-export): the tideline behind `/tideline` - [Plan mode and todos](/oceancode/plan-mode-and-todos): `/plan` and the todo pane - [Bounding a long run](/oceancode/bounding-a-long-run): `/budget` and `/limits` # Work modes Source: https://www.plungeai.com/docs/oceancode/work-modes A work mode decides how agents work together on your prompt. There are three answers to "who decides?": - **The model** — **Model**, the default. The model decides on its own whether to work alone, call sub-agents or run a workflow. This is OceanCode as it was before work modes: every tool is offered, any sub-agent type may be called, nothing is added to the prompt, there are no new limits and no badge. - **Jev** — **Auto**. Once per prompt, Jev picks one of the modes below; when it cannot, simple rules pick. - **You** — one of seven diagrams: Solo, Delegate, Pair, Pipeline, Orchestrate, Goal, Race. Nothing needs setting up. Until you pick another mode, nothing changes. ## Picking a mode `F4` or `/mode` opens the picker: the modes on the left with the highlighted one's description under them, its picture and cost on the right. Model is first; the configured default (Model unless your config's `mode` names another) is tagged `default`; your own diagrams follow a `── custom ──` divider. | Key | What it does | |---|---| | `↑` / `↓` (`k` / `j`) | Move. The picture follows at once. | | `Enter` | Use this mode for this session. | | `d` | Make it the current agent's default: writes `agents.<id>.mode` to your user `oceancode.json` (the footer names the agent: `d: default for Build`). | | `e` | Copy a built-in to `.oceancode/modes/<id>-copy.jsonc` to edit (toast: `Copied to <path>`). | | `Esc` | Close without a change. | Typed forms: `/mode <id>` sets this session's mode, `/mode default` clears it. The toast reads `Mode: <label>`; an unknown id is refused with the list of known ones (`crates/ocean-acp/src/extensions/mode.rs:98`). **Which mode a session runs**, first match wins (`crates/ocean-session/src/run/mode.rs:55`): 1. the session's own pick (`Enter`, `/mode <id>`); 2. `agents.<id>.mode` for the session's agent (`d`, or F2 › Agent defaults); 3. the `mode` key in `oceancode.json`; 4. `model`. `workflows.orchestrate: "always"` with no `mode` set is read as `mode: "goal"`, and logs that once. A sub-agent always runs under its lead's mode; `/mode` on a sub-agent's session is refused. `agents.<id>.mode` in `oceancode.json` is the work mode. It is not the `mode:` line of an agent's markdown file, which says where the agent may run (`primary`, `subagent`, `all`). A config row that sets `"mode": "subagent"` names no diagram, so that agent runs Model, with a log line. **The badge.** Every mode but Model shows a badge on the prompt's top border: `solo`, `delegate ≤4`, `pair`, `pipeline`, `orchestrate ≤8`, `goal`, `race ×3`. Auto shows `auto` until its first pick, then the pick and why: `auto → goal · Jev 0.82` or `auto → model · rules: default` (`crates/ocean-protocol/src/diagram.rs:590`). Model shows no badge, so the border looks as it always did. ## The modes Model, Solo, Delegate, Pair and Orchestrate are **lead** modes: your prompt is an ordinary turn, and the mode only shapes what the lead may do. Pipeline, Goal and Race are **script** modes: each prompt you type becomes the task of a workflow run. The turn answers at once (`race ×3 — started run <id>`) and the result arrives later in the session. Delegate, Pair and Orchestrate also add a short block to the lead's prompt naming the agents it may call and the limits. ### Model — the model decides ``` you │ ┌─────────▼──────────┐ │ Lead ✎ │ │ decides: alone, │ │ sub-agents or a │ │ workflow │ └─────────┬──────────┘ ▼ you ``` Nothing is added or hidden, on any provider. Sub-agents may nest 4 deep, as before; only numbers you set in `subagent.max_*` limit it further. ### Auto — Jev picks per task ``` you │ ◇ Jev picks a diagram │ (the rules, if Jev is off) ▼ the picked diagram runs ``` See [Auto](#auto) below. ### Solo — one agent does everything ``` you │ ┌─────────▼──────────┐ │ Lead ✎ │ │ reads · edits · │ │ runs the tests ↺ │ └─────────┬──────────┘ ▼ you ``` One model, one context. The lead is not offered `task`, `send_subagent_message` or `workflow`, and works off its plan until every row is done. For small or tightly connected changes. Cost ≈ 1×. ### Delegate — a lead that calls helpers ``` you │ ┌───────▼───────┐ │ Lead ✎ │ └───────┬───────┘ ┌────────────┼────────────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ explore │ │ general │ │ review │ └─────────┘ └─────────┘ └─────────┘ ``` The lead does the work and sends side questions to helpers, which return evidence (`path:line`). `task` takes only these three types; another is refused with `mode Delegate allows: explore, general, review`. Up to 4 at once. For everyday work. Cost ≈ 1.2–2×. ### Pair — the lead decides, one builder writes ``` you │ ┌──────▼─────┐ spec ┌────────────┐ │ Lead │────────▶│ builder ✎ │ │ decides, │◀────────│ edits, runs│ │ reads diff │ diff │ the checks │ └──────┬─────┘ └────────────┘ │ optional ┌────▼────┐ │ explore │ └─────────┘ ``` The lead never edits (`edit`, `write`, `apply_patch` and `hashline` are taken away). It writes a change spec, one builder edits and runs the checks, and the lead reads the diff and accepts it or sends corrections. The builder keeps its context between hand-offs; a second running builder is refused (`bound: max_builder 1/1`). For a strong thinker with a faster, cheaper coder. Cost ≈ 1.3–2×. ### Pipeline — spec, your approval, build, review ``` you ┌─────────▼─────────┐ │ Spec · explore │ └─────────┬─────────┘ ◇ you approve ┌─────────▼─────────┐ │ Build ✎ · builder │◀─┐ └─────────┬─────────┘ │ fix list ┌─────────▼─────────┐ │ ↺ ≤ 2 │ Review · review │──┘ └─────────┬─────────┘ ▼ you ``` Fixed stages, each its own agent. Nothing is built until you approve the spec in a question card; review findings go back to the builder at most twice. For features where the spec should be agreed first. Cost ≈ 1.5–3×. ### Orchestrate — a workflow per task ``` you │ ┌─────▼──────┐ │ Lead │ └─────┬──────┘ ├─ 1 scouts explore ×N ├─ 2 plan from the evidence ├─ 3 build builder ✎ ◀─┐ ├─ 4 checks review ×3 ──┘ ↺ ≤ 2 └─ 5 report general ──▶ you ``` For a substantive task the lead runs a workflow in this shape (the built-in `implement` diagram, or a script it writes), adjusted to the task. Short questions stay plain turns. Up to 8 at once. For specs, research and large changes with several parts. Cost ≈ 3–15×. ### Goal — loop until proven done ``` you: objective │ ┌──────▼──────┐ │ Plan │ └──────┬──────┘ ┌──────▼──────┐ ┌─▶│ Worker ✎ │ │ └──────┬──────┘ │ ◇ 3 skeptics check └─not yet─┤ ↺ ≤ 10 ▼ done · paused · bound reached ──▶ you ``` The built-in `goal` workflow: it plans deliverables you can check, then one worker, resumed every pass, works until a panel of skeptics agrees it is done. At most 10 passes; it pauses when the same gaps come back twice. Goal writes no limits of its own: the workflow bounds apply (`workflows.max_concurrent_agents`, `workflows.agent_budget`, [02](/oceancode/configuration)). For long tasks with a clear finish line. Cost ≈ 5–20×. ### Race — attempts, a judge ranks, you pick ``` you: task ┌─────────────┼─────────────┐ ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ │ Attempt A │ │ Attempt B │ │ Attempt C │ │ builder ✎ │ │ builder ✎ │ │ builder ✎ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ ◇ tests ◇ tests ◇ tests └─────────────┼─────────────┘ ┌─────▼─────┐ │ judge │ └─────┬─────┘ ◇ you pick ──▶ applied ``` Three attempts, each running the sub-agent model (else the session's) in its own copy of the project. The tests run in every copy, the judge ranks the attempts on their diffs and test results, and you pick one (or `None`). Nothing touches your project until you pick; the winner is applied as a patch. For important changes with tests. Cost ≈ 3× plus the judge. `builder`, `review` and `judge` are built-in agents, hidden from every agent list; `review` and `judge` are read-only. Your own agent file or config row with a `prompt` or `description` under one of these names replaces the built-in; a row that sets only `model` or `effort` tunes it (`crates/ocean-agent/src/agentdef/agent.rs:238`). ## Limits A mode can bound sub-agents three ways: how many run **at once** in the whole tree, how many one **turn** may start, and how **deep** they nest. For each number, your `subagent.max_*` setting wins, then the mode's own, else there is none; depth falls back to 4 (`crates/ocean-protocol/src/diagram.rs:272`). | Mode | At once | Per turn | Depth | |---|---|---|---| | Model | none | none | 4 | | Solo | 0 (no sub-agents) | none | 4 | | Delegate, Pair, Pipeline | 4 | 30 | 2 | | Orchestrate | 8 | 30 | 2 | | Goal | none (the workflow bounds apply) | none | 4 | | Race | 3 | 30 | 2 | | Auto | the pick's | the pick's | the pick's | How each limit acts (`crates/ocean-session/src/run/spawn_governor.rs:156`): - **At once** covers the lead and every sub-agent under it. When no slot is free, the lead's spawn (or a workflow's) waits, and the dock shows a `waiting (4/4 running)` row until a slot frees. A deeper sub-agent's spawn is refused rather than queued (`bound: max_concurrent 4/4`), because its caller already holds a slot. At `0` the `task` tool is not offered at all. - **Per turn** counts spawns since your last prompt. At the limit a question card asks `Sub-agents this turn reached 30. Continue?` — `Continue (+30)` allows 30 more, `Stop spawning` refuses the rest of the turn. With no one to ask (a headless run) the spawn is refused: `bound: max_per_turn 30/30`. - **Depth**: at the limit the `task` tool is not offered, and a spawn is refused with the depth message it always had. - **Per type**: a diagram may limit one agent type (`"max": 1` in Pair): `bound: max_builder 1/1`. A refused spawn reaches the model as that `task` call's error; the turn goes on. Switching modes gives the new limit to the next spawn, while children already running keep their old slots, so for a moment old and new can run side by side. Edit the three numbers under F2 › Sub-agents, or in `oceancode.json` ([02](/oceancode/configuration)). ## Auto Once per prompt, Jev answers one typed question: which of the modes that can run here fits the task best (`crates/ocean-session/src/run/work_mode_jev.rs:120`). It answers within Jev's deadline (`jev.deadline_ms`, 1.5 s by default); the first Auto prompt of a process can take longer while the connection opens. Jev's pick runs when its confidence is at least `jev.thresholds.work_mode_confidence` (0.6). With Jev off, no TypeSafe key, an error, a time-out or lower confidence, the rules pick instead (`crates/ocean-protocol/src/auto_route.rs:22`): 1. three or more file paths, or three or more list lines → Orchestrate; 2. a finish line (`until`, `tests pass`, `make … pass`, `suite`) → Goal; 3. one line of fewer than 25 words that is not a question → Solo; 4. anything else → Model. The badge says which decided: `auto → goal · Jev 0.82`, or `auto → solo · rules: short task`. Auto never picks a mode that cannot run here (see [Requirements](#requirements)). `/mode` overrides it. Every Jev decision is a row in `jev-decisions.jsonl` under the point `work_mode`, which is asked only under Auto ([13](/oceancode/jev-decisions)). ## Requirements - **Pipeline, Goal and Race** need workflows on (`workflows.enabled`, the default). - **Race** needs a git project and a `test_command` in `oceancode.json`. - **Pipeline and Race** need an interactive session, because they ask you to approve or pick; headless runs refuse them. A mode that cannot run answers the prompt with the reason, for example `race needs a git project` or `pipeline needs an interactive session` (`crates/ocean-session/src/run/mode.rs:122`). **Race copies.** Each attempt runs in a `git clone --local` of the project under the run's directory (`<data dir>/workflows/<run id>/copies/`, `crates/ocean-session/src/run/workflow/launcher.rs:353`), never inside the project. The copy carries your uncommitted changes and untracked files (ignored ones stay out). The winner comes back as one patch applied to the working tree only — nothing is staged — and a patch that does not apply changes nothing; the result then names the copy, which is kept. The other copies are removed when the run ends; a paused run keeps them. Copies of a run that never ended (OceanCode quit or crashed mid-race, or a pause never resumed) are removed by a later OceanCode once the run has sat untouched for a day (`crates/ocean-session/src/run/workflow/launcher.rs:498`). A copy does not include submodules, an absolute symlink in it still points at your real project, and Git LFS may fetch while the copy is made (`crates/ocean-session/src/run/workflow/race_copy.rs:48`). **Trust.** Commands from project files — `test_command`, and a custom diagram step's `command` — run through the same `bash` permission rules as the model's own commands, without a separate repo-trust check. That is how the `formatter` setting already behaves. Race runs a diff and the tests in each copy through those rules, so with `bash: ask` it asks for each one: six questions for three attempts. ## Custom diagrams A diagram is one `.jsonc` (or `.json`) file. OceanCode reads `<project>/.oceancode/modes/` and `<config-dir>/oceancode/modes/` (`crates/ocean-app/src/modes.rs:28`). The quickest start is `e` in the picker, which copies a built-in to `<id>-copy.jsonc`; edit it and save, and the picker has it on its next open. The folder is watched even when it is created after start. ```jsonc { "id": "review-first", // lowercase letters, digits and dashes "label": "Review first", "summary": "explore, then build, then review", "flow": "script", // "lead" or "script" "agents": { "explore": {}, "builder": {}, "review": {} }, "steps": [ { "agent": "explore", "do": "Find what this touches:\n{task}", "as": "scan" }, { "agent": "builder", "do": "Make the change:\n{task}\n\n{scan}" }, { "agent": "review", "do": "Review the change", "as": "review", "schema": "verdict" } ], "limits": { "at_once": 2, "per_turn": 20 } } ``` - A file is keyed by its `id`. A built-in id cannot be reused, and when two files share an id the project's wins; the other file is skipped. - `agents` names real agent ids. A lead diagram may set `"lead_writes": false` and a `lead_prompt`. A step is an `agent`, a `parallel` step (`"n"`, or a `"models"` list), a `loop` (it needs `"max"`) or a `gate` (`approve`, `pick`, `tests`); two writers that run at once each need `"isolate": "copy"`, which needs git. - `picture` is optional: without one the picker shows a line per step. `"picker": false` keeps a diagram out of the picker. - A file that does not parse or validate is left out, and the engine log names the file and the reason ([18](/oceancode/operations#log-levels-oc_log)). A session whose custom mode's file was deleted runs Model. ## See also - [02 · Configuration](/oceancode/configuration) — `mode`, `test_command`, `agents.<id>.mode`, `subagent.max_*`. - [13 · Jev decisions](/oceancode/jev-decisions) — the `work_mode` point. - [19 · Ocean pager](/oceancode/pager) and [22 · Ocean pager reference](/oceancode/pager-reference) — the picker, the badge, F2 and the dock. # HTTP API reference Source: https://www.plungeai.com/docs/oceancode/http-api `oceancode serve` runs the engine as a long-lived HTTP server, by default on `http://127.0.0.1:4096` (`--port` and `--host` change it; the default pager shape takes `--listen <host:port>`). Every route below is relative to that base URL. HTTP basic auth is off until `OC_SERVER_PASSWORD` is set; then it covers every route, SSE included, with the username from `OC_SERVER_USERNAME` (default `oceancode`). A non-loopback bind refuses to start without the password. Events stream over SSE: `/event` serves the v1 frames, `/api/event` and `/global/event` the v2 payload-wrapped envelope. Flags and variables: [Headless and server](/oceancode/headless-and-server). ```bash curl -u "oceancode:$OC_SERVER_PASSWORD" http://127.0.0.1:4096/api/health ``` ## v2 — /api | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/api/agent` | `v2.agent.list` | built-in + config agents. | | GET | `/api/command` | `v2.command.list` | — | | PATCH | `/api/credential/{credentialID}` | `v2.credential.update` | (relabel). The contract declares 204. | | DELETE | `/api/credential/{credentialID}` | `v2.credential.remove` | The contract declares 204. | | GET | `/api/event` | — | The v2 `GlobalEvent` stream shared by `GET /api/event` (`v2.event.subscribe`) and `GET /global/event` (`global.event`). | | GET | `/api/fs/find` | `v2.fs.find` | ranked recursive search. | | GET | `/api/fs/list` | `v2.fs.list` | the direct children of a directory. | | GET | `/api/fs/read/{*path}` | `v2.fs.read` | The wildcard path is the file to read; the body is the raw bytes under the detected mime. | | GET | `/api/health` | `v2.health.get` | — | | GET | `/api/integration` | `v2.integration.list` | — | | GET | `/api/integration/attempt/{attemptID}` | `v2.integration.attempt.status` | An unknown attempt is a 404 `NotFoundError`. | | DELETE | `/api/integration/attempt/{attemptID}` | `v2.integration.attempt.cancel` | The contract declares 204. | | POST | `/api/integration/attempt/{attemptID}/complete` | `v2.integration.attempt.complete` | The contract declares 204. | | GET | `/api/integration/{integrationID}` | `v2.integration.get` | — | | POST | `/api/integration/{integrationID}/connect/key` | `v2.integration.connect.key` | The contract declares 204. | | POST | `/api/integration/{integrationID}/connect/oauth` | `v2.integration.connect.oauth` | — | | GET | `/api/location` | `v2.location.get` | The `LocationInfo` schema requires a resolved `{directory, project}` (project carries id + directory), so it is built via the shared helper rather than a bare `{directory}`. | | GET | `/api/model` | `v2.model.list` | — | | GET | `/api/permission/request` | — | the pending requests wrapped in `{location, data}`. | | GET | `/api/permission/saved` | — | the always-allow rules as `PermissionSavedInfo`s wrapped in `{data}`. | | DELETE | `/api/permission/saved/{id}` | — | drops a saved rule addressed by the composite `"<permission>:<pattern>"` id. | | GET | `/api/provider` | `v2.provider.list` | — | | GET | `/api/provider/{providerID}` | `v2.provider.get` | — | | GET | `/api/pty` | `v2.pty.list` | — | | POST | `/api/pty` | `v2.pty.create` | — | | GET | `/api/pty/{ptyID}` | `v2.pty.get` | — | | PUT | `/api/pty/{ptyID}` | `v2.pty.update` | — | | DELETE | `/api/pty/{ptyID}` | `v2.pty.remove` | The v2 contract is 204. | | GET | `/api/pty/{ptyID}/connect` | `v2.pty.connect` | (WebSocket upgrade). | | POST | `/api/pty/{ptyID}/connect-token` | `v2.pty.connectToken` | — | | GET | `/api/question/request` | — | the `{location, data}` envelope over the request list. | | GET | `/api/reference` | `v2.reference.list` | — | | GET | `/api/session` | `v2.session.list` | — | | POST | `/api/session` | `v2.session.create` | — | | GET | `/api/session/active` | `v2.session.active` | The sessions with an in-flight coordinator drain, keyed by id. | | GET | `/api/session/{sessionID}` | `v2.session.get` | — | | POST | `/api/session/{sessionID}/agent` | `v2.session.switchAgent` | Publishes the durable agent-switched event the projector already handles. | | POST | `/api/session/{sessionID}/compact` | `v2.session.compact` | Triggers a compaction turn via `Runner::compact` (the same summarize-then-replace path the runner's own overflow recovery uses). 204 on success; errors map through the shared session-error writer. | | GET | `/api/session/{sessionID}/context` | `v2.session.context` | The active post-compaction context window, wrapped in `{data:[…]}`. | | POST | `/api/session/{sessionID}/interrupt` | `v2.session.interrupt` | the v2 alias of the v1 `session.abort` semantics. | | GET | `/api/session/{sessionID}/message` | `v2.session.messages` | — | | GET | `/api/session/{sessionID}/message/{messageID}` | `v2.session.message` | — | | POST | `/api/session/{sessionID}/model` | `v2.session.switchModel` | — | | GET | `/api/session/{sessionID}/permission` | — | the pending requests scoped to one session. | | POST | `/api/session/{sessionID}/permission` | — | evaluates the requested action/resources against the session's ruleset synchronously (no blocking wait) and returns `{id, effect}`, mirroring the v2 contract's non-interactive evaluation shape. | | GET | `/api/session/{sessionID}/permission/{requestID}` | — | a single pending request. | | POST | `/api/session/{sessionID}/permission/{requestID}/reply` | — | — | | GET | `/api/session/{sessionID}/permission/mode` | — | the session's mode + lockout. | | POST | `/api/session/{sessionID}/permission/mode` | — | sets the mode from `{ "mode": "acceptEdits" }`; 400 on an unknown mode, 409 when bypass is locked. | | POST | `/api/session/{sessionID}/prompt` | `v2.session.prompt` | Admits durable user input (the `session.next.prompted` event) then drives the coordinator to run the turn. The drain runs `Runner::run`, which streams the provider turn and projects the assistant message. | | GET | `/api/session/{sessionID}/question` | — | — | | POST | `/api/session/{sessionID}/question/{requestID}/reject` | — | TS validates the path params BEFORE the handler resolves the session, so a malformed `requestID` is a 400 even for a session that does not exist. | | POST | `/api/session/{sessionID}/question/{requestID}/reply` | — | — | | POST | `/api/session/{sessionID}/revert/clear` | — | the v2 alias of `Store::unrevert`. | | POST | `/api/session/{sessionID}/revert/commit` | — | The ported store has no durable "committed" state distinct from "staged" (unlike upstream's stage/commit split, which also touches the working tree), so commit is a no-op success once a bookmark is confirmed to exist — staging already persisted the revert. | | POST | `/api/session/{sessionID}/revert/stage` | — | Faithful to the v1 revert semantics (`Store::revert` stages a bookmark at `messageID`); `files` (a diff/file-list request) is accepted but the ported `RevertState` carries no diff/files projection, so those fields are omitted rather than fabricated. | | POST | `/api/session/{sessionID}/wait` | `v2.session.wait` | TS's `V2Session.wait` is a stub: after confirming the session exists it unconditionally fails with `Session.OperationUnavailableError`, mapped to a 503 `ServiceUnavailableError`. Match the status and message exactly so a client cannot tell the servers apart — the existence check still produces 404 for an unknown id first. | | POST | `/api/session/{sessionID}/subagent/{childID}/message` | — | Steers (`delivery:"steer"`) or queues (`"queue"`, the default) `text` into the child's live turn, or resumes it when it has settled. Returns the delivery status. A missing child is a 404. | | POST | `/api/session/{sessionID}/subagent/{childID}/resume` | — | Re-enters a completed child with `text` on a fresh step budget and returns its new result. A missing child is a 404. | | GET | `/api/skill` | `v2.skill.list` | — | ## v1 — auth / catalog / config | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/agent` | `app.agents` | the v1 shape. | | PUT | `/auth/{providerID}` | `auth.set` | persist a credential to `auth.json`. | | DELETE | `/auth/{providerID}` | `auth.remove` | drop the stored credential. | | GET | `/command` | `command.list` | the v1 shape. | | GET | `/config` | `config.get` | the merged config. | | PATCH | `/config` | `config.update` | Deep-merges the body over the project config document and persists it. The merged value is written back into `Services` so the cached read path (`config.get`, `formatter.status`, agent/command discovery) reflects the change. TS `ConfigHttpApi.update` returns `ctx.payload` — the SUBMITTED patch, not the merged config — so the request body is echoed. | | GET | `/config/providers` | `config.providers` | TS `config.providers` returns `providerSvc.list()` — the RESOLVED, connected providers (those with a usable credential via `auth.json`, a stored DB credential, or a declared env key) — plus a per-provider default-model map. It is NOT the bare `oceancode.json` `provider` override block: the TUI's connect gate iterates this list, and an override-only answer left the gate permanently false. | | GET | `/event` | `event.subscribe` | the v1 SSE stream (`{id,type,properties}` frames). Mirrors `EventSubscribe`. | ## experimental | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/experimental/capabilities` | `experimental.capabilities.get` | Mirrors `experimental.ts capabilities()`: `{backgroundSubagents: …}`: on unless `OC_EXPERIMENTAL_BACKGROUND_SUBAGENTS` is `0`/`false` — the same rule the task tool's background mode reads. | | GET | `/experimental/console` | `experimental.console.get` | Mirrors `experimental.ts getConsole()`: `{consoleManagedProviders, activeOrgName?, switchableOrgCount}`. With no cloud accounts registered `account.orgsByAccount()` returns `[]` (`account.ts:329` over an empty `repo.list()`), so `switchableOrgCount` is 0; `consoleManagedProviders` is `[]` (`config.ts:589`); `activeOrgName` is omitted. No 500 — the empty result is a success. | | GET | `/experimental/console/orgs` | `experimental.console.listOrgs` | Mirrors `experimental.ts listConsoleOrgs()`: `{orgs: [...]}`. With no accounts, `orgsByAccount()` is `[]` so the flattened list is empty (`experimental.ts:56`). | | POST | `/experimental/console/switch` | `experimental.console.switchOrg` | Mirrors `experimental.ts switchConsole()`: `account.use(accountID, orgID)` wrapped in a catch that maps any failure to a 400 `BadRequest` (`experimental.ts:84`). With no account registered `use()` fails, so this is the faithful 400. | | POST | `/experimental/control-plane/move-session` | `experimental.controlPlane.moveSession` | Mirrors `control-plane.ts moveSession()`: 204 on success; failures map to `MoveSessionError`. `MoveSession` first loads the session (`move-session.ts:77`) — a missing one yields `"Session not found: {id}"` (`control-plane.ts:33`). The move itself is a local operation and is a no-op success when the destination is the session's current directory (`move-session.ts:79`). | | POST | `/experimental/project/{projectID}/copy` | `v2.projectCopy.create` | — | | DELETE | `/experimental/project/{projectID}/copy` | `v2.projectCopy.remove` | The contract declares 204. | | POST | `/experimental/project/{projectID}/copy/generate-name` | `experimental.projectCopy.generateName` | The LLM naming path is not ported; a deterministic slug is returned, matching the `{name}` shape. | | POST | `/experimental/project/{projectID}/copy/refresh` | `v2.projectCopy.refresh` | The contract declares 204. | | GET | `/experimental/resource` | `experimental.resource.list` | Mirrors `experimental.ts resource()`: `mcp.resources() -> Record<id, Resource>`. The MCP manager bridges tools but does not enumerate resources, so a fresh instance returns the empty map (`experimental.ts:169`). | | GET | `/experimental/session` | `experimental.session.list` | Mirrors `experimental.ts session()`: `sessions.listGlobal(...) -> GlobalSession[]`. The global cross-project aggregate (slug/summary/cost/tokens/project) is not ported; a fresh self-hosted instance's `listGlobal` yields `[]` (`experimental.ts:134`). | | POST | `/experimental/session/{sessionID}/background` | `experimental.session.background` | Mirrors `experimental.ts sessionBackground()`: returns `false` immediately when the `experimentalBackgroundSubagents` runtime flag is off (`experimental.ts:156`). That flag is off by default. | | GET | `/experimental/tool` | `tool.list` | every registered tool's id/description/parameters. | | GET | `/experimental/tool/ids` | `tool.ids` | — | | GET | `/experimental/workspace` | `experimental.workspace.list` | — | | POST | `/experimental/workspace` | `experimental.workspace.create` | — | | GET | `/experimental/workspace/adapter` | `experimental.workspace.adapter.list` | Mirrors `workspace.ts adapters() -> listAdapters(projectID)` (`workspace.ts:16`). The only built-in is the worktree adapter (`control-plane/adapters/index.ts BUILTIN`), reported with its name/description (`adapters/worktree.ts:29-30`). | | GET | `/experimental/workspace/status` | `experimental.workspace.status` | Mirrors `workspace.ts status()`: `[...connections.values()]` filtered to the project's workspaces (`workspace.ts:55, 833`). With no active sync connections the map is empty. | | POST | `/experimental/workspace/sync-list` | `experimental.workspace.syncList` | Mirrors `workspace.ts syncList()`: discover workspaces via the registered adapters and register any new ones, no body, 204 (`workspace.ts:51, 743`). The built-in worktree adapter discovers nothing without configured remotes, so this is a no-op 204. | | POST | `/experimental/workspace/warp` | `experimental.workspace.warp` | Mirrors `workspace.ts warp()`: `workspace.sessionWarp(...)` where a missing workspace maps to a 404 `NotFoundError` (`workspace.ts:81`). With no synced workspaces the target is always absent. | | DELETE | `/experimental/workspace/{id}` | `experimental.workspace.remove` | — | | GET | `/experimental/worktree` | `worktree.list` | the copy worktree directories. | | POST | `/experimental/worktree` | `worktree.create` | a linked worktree on a new branch under the repo of the request directory. | | DELETE | `/experimental/worktree` | `worktree.remove` | INTENTIONAL, SAFER DIVERGENCE FROM TS, carried verbatim from Go: `git worktree remove` refuses to remove the repository's MAIN worktree and that refusal surfaces as a 400. The TS handler is permissive and reports 200 for the same request because its remove path does not guard the main worktree. The guard is kept — matching TS would mean performing (or claiming) a destructive removal of the primary checkout. The non-main path is identical (200) on all three servers. | | POST | `/experimental/worktree/reset` | `worktree.reset` | hard-reset a linked worktree (the body's directory) back to the default branch of the repo identified by the request directory. | ## files / search | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/file` | `file.list` | direct children as v1 `FileNode`s. | | GET | `/file/content` | `file.read` | The TS v1 `/file/content` returns only `{type, content}` (no mimeType) and trims the decoded text; both are mirrored so the envelope matches byte-for-byte. | | GET | `/file/status` | `file.status` | the working-tree git status mapped onto the v1 `File` shape. | | GET | `/find` | `find.text` | ripgrep-style file-content search rooted at the request directory. `pattern` is the regexp; the results are the ripgrep `match` records the TS handler forwards verbatim. | | GET | `/find/file` | `find.files` | ranked file-name search returning the matching paths. | | GET | `/find/symbol` | `find.symbols` | The LSP client exposes hover/definition/diagnostics but not `workspace/symbol`, and no live client is retained, so symbol search has no backend; the endpoint returns an empty-but-valid array. | | GET | `/formatter` | `formatter.status` | the per-formatter enabled/extensions status computed from config. | ## global / instance | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/global/config` | `global.config.get` | — | | PATCH | `/global/config` | `global.config.update` | TS `GlobalHttpApi.configUpdate` persists and returns only the submitted patch projection (an empty patch yields `{}` after provider/default stripping), not the full merged config. Echo the request body. | | POST | `/global/dispose` | `global.dispose` | Mirrors `global.ts dispose()`: returns `true` after disposing. The server is a single long-lived process, so there is nothing to tear down per request (the OS process owns the lifecycle). | | GET | `/global/event` | — | The v2 `GlobalEvent` stream shared by `GET /api/event` (`v2.event.subscribe`) and `GET /global/event` (`global.event`). | | GET | `/global/health` | `global.health` | Mirrors `global.ts health()`: `{healthy: true, version}`, plus the four fields this port adds so every running instance says what it is: `model_format` is always `"markdown"` — the only payload format the product renders since D9; the field stays because clients read it, but it is no longer configurable — `ledger` says whether the graph's Session Ledger hook is installed (`ocean_app::Services::ledger`) — leaf T48b / T52b — `build` is the binary's own build stamp (`crate::server_build`, leaf T65), and `dropped_events` is the bus's drop-oldest counter (`ocean_events::Bus::dropped_events`, long-runs plan Task 13 / L-12): live events evicted from a saturated subscriber queue since boot — a saturation signal, not an error count, and never reset. | | POST | `/global/upgrade` | `global.upgrade` | Mirrors `global.ts upgrade()`: when the installation method is `"unknown"` it reports `{success:false, error:"Unknown installation method"}`. This build is not produced by a known installer. The contract models this as the failure variant of the 200 `anyOf` (the 400 is reserved for the BadRequest envelope), so it is returned with status 200. | | POST | `/instance/dispose` | `instance.dispose` | Mirrors `instance.ts dispose()`: returns `true`. Single-process, so no per-request teardown. | | POST | `/log` | `app.log` | write the structured log line then return true. | ## lsp / mcp | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/lsp` | `lsp.status` | The registry resolves candidate servers but does not hold live clients (spawn is on demand), so there is no connected server to report; the endpoint returns an empty-but-valid array. | | GET | `/mcp` | `mcp.status` | a map of server name -> status. | | POST | `/mcp` | `mcp.add` | register a server at runtime and connect it, returning the full status snapshot. | | POST | `/mcp/{name}/auth` | `mcp.auth.start` | begin the remote OAuth flow, returning the authorization URL the caller opens. | | DELETE | `/mcp/{name}/auth` | `mcp.auth.remove` | The 200 body is `{"success": true}` (`additionalProperties:false`), not a bare boolean. | | POST | `/mcp/{name}/auth/authenticate` | `mcp.auth.authenticate` | — | | POST | `/mcp/{name}/auth/callback` | `mcp.auth.callback` | completes the remote OAuth flow with the code returned to the loopback callback and answers with the server's resulting status. | | POST | `/mcp/{name}/connect` | `mcp.connect` | — | | POST | `/mcp/{name}/disconnect` | `mcp.disconnect` | — | | GET | `/mcp/{name}/prompts` | — | the server's cached prompt list (P1-T38). | | GET | `/mcp/{name}/resources` | — | the server's advertised resources (P1-T38), via `McpManager::list_resources` → `Client::list_resources`. | | GET | `/mcp/{name}/tools` | — | the server's cached tool definitions (WP6d). | | POST | `/mcp/{name}/trust` | `mcp.trust` | (GAP-06): persist the per-server trust decision to `<home>/.oceancode/trust.json` (keyed by the project dir) and apply it to the running manager, returning the refreshed `GET /mcp` map. | | POST | `/mcp/{name}/enable` | `mcp.enable` | (GAP-06 parity): persist the enable/disable decision to `<home>/.oceancode/trust.json`'s `disabled_mcp` (so the next boot honours it) and apply it live to the running manager, returning the refreshed `GET /mcp` map. An unknown server id is a 404. | ## hooks (GAP-06) | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/hooks` | — | the boot-time hook source report plus this project's repo trust bit (GAP-06). Read-only: the rows are whatever the boot loader (or the last reload) recorded. | | POST | `/hooks` | — | add a command hook to one of Claude Code's three writable settings files (GAP-06 parity), then reload the dispatcher and return the refreshed `GET /hooks` shape. An unknown event / empty command is a 400 (`InvalidInput`); a settings file that is not a JSON object is a 400 (`InvalidData`, never clobbered). The body is an `ocean_app::hooks_boot::HookEdit`. | | POST | `/hooks/reload` | — | re-run the boot hook loader (re-attaching the dispatcher) and return the refreshed report (GAP-06). | | POST | `/hooks/remove` | — | remove a hook from one of the three writable settings files (GAP-06 parity), then reload and return the refreshed `GET /hooks` shape. A path that is not one of the three writable scopes is a 400 (`InvalidInput`); a handler that is not present is a 404 (`NotFound`). The body is an `ocean_app::hooks_boot::HookRemove`. | | POST | `/trust/repo` | — | flip this project's repo-tier trust (GAP-06): persist the decision, release/hold the repo's `.mcp.json` servers and reload the hook dispatcher, then return the refreshed `GET /hooks` shape. A trust-store write error is a 400. | ## the Claude Code settings mirror (2026-09-10) | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/claude/settings` | — | the Claude Code settings mirror (`model`, `effortLevel`/`modelSettings`, `ultracode`, `alwaysThinkingEnabled` of the three tiered `settings.json` files) with the effective effort and the model it pins (`ocean_app::claude_settings`, 2026-09-10). Read-only: the rows are whatever boot or the last watcher reload recorded. | ## path / permission | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/path` | `path.get` | The home/state/config values mirror the TS server's `Global.Path.*`, derived via the xdg-basedir port in `ocean-config` so they are byte-identical to the TS server on the same host/env (e.g. `~/.config/oceancode`, not the macOS `os.UserConfigDir()` location). | | GET | `/permission` | — | the v1 alias, a bare array. | | POST | `/permission/{requestID}/reply` | — | the v1 alias, dispatched on `requestID` alone. | ## plugin | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/plugin` | — | the installed plugins with their enable/trust bits. | | POST | `/plugin/install` | — | install from `source`, returning `{name, version, enabled, trusted}`. | | GET | `/plugin/marketplace` | — | the added marketplaces with their plugin entries. | | POST | `/plugin/marketplace/add` | — | add a marketplace by spec. | | DELETE | `/plugin/marketplace/{name}` | — | remove a marketplace, returning the refreshed marketplace list (GAP-06 parity). A marketplace that is not present is a 404. | | POST | `/plugin/{name}/enable` | — | turn a plugin's loading on or off. | | POST | `/plugin/{name}/trust` | — | grant or revoke a plugin's execution trust. A missing/invalid body is a 400 (`falsify_trust_route_requires_body`), so trust is never granted by an empty request. | | GET | `/plugin/{name}` | — | the full detail of one installed plugin (GAP-06 parity). A name that is not installed is a 404. | | DELETE | `/plugin/{name}` | — | uninstall a plugin, returning the refreshed installed list (GAP-06 parity). A plugin that is not installed is a 404. | ## project | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/project` | `project.list` | — | | GET | `/project/current` | `project.current` | resolve the request directory. | | POST | `/project/git/init` | `project.initGit` | initialize a repository in the request directory (idempotent). | | PATCH | `/project/{projectID}` | `project.update` | — | | GET | `/project/{projectID}/directories` | `project.directories` | — | ## provider | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/provider` | `provider.list` | `{all, default, connected}`. | | GET | `/provider/auth` | `provider.auth` | Mirrors `provider.ts auth()`: `svc.methods() -> Record<providerID, Method[]>`. No v1 auth-method plugins are ported, so this is the empty map a fresh instance returns. | | POST | `/provider/{providerID}/oauth/authorize` | `provider.oauth.authorize` | Mirrors `provider.ts authorize()`: with no oauth method registered for the provider the TS service hits an undefined method lookup, which the handler maps to a generic `BadRequest` `ProviderAuthError`. The Claude Pro/Max subscription login is served elsewhere (the v2 integration endpoints and the `oceancode login` CLI), so a v1 client is never offered a method here. | | POST | `/provider/{providerID}/oauth/callback` | `provider.oauth.callback` | Mirrors `provider.ts callback()`: with no pending oauth result the TS service returns `OauthMissing`. | ## pty | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/pty` | `pty.list` | — | | POST | `/pty` | `pty.create` | — | | GET | `/pty/shells` | `pty.shells` | the available login shells. | | GET | `/pty/{ptyID}` | `pty.get` | — | | PUT | `/pty/{ptyID}` | `pty.update` | — | | DELETE | `/pty/{ptyID}` | `pty.remove` | The v1 contract answers 200 `true` (unlike the v2 delete, which is 204). | | GET | `/pty/{ptyID}/connect` | `pty.connect` | (WebSocket upgrade). | | POST | `/pty/{ptyID}/connect-token` | `pty.connectToken` | — | ## question | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/question` | — | every pending question across sessions. | | POST | `/question/{requestID}/reject` | — | dismisses a pending question, failing the waiting `Ask` with a rejection. | | POST | `/question/{requestID}/reply` | — | resolves a pending question with the user's answers, unblocking the waiting `Ask`. | ## session (v1) | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/session` | `session.list` | The v1 response is the bare array. | | POST | `/session` | `session.create` | Maps the supported v1 body fields onto `SessionStore::create`; `parentID`, `metadata` and `permission` have no ported column and are dropped. | | GET | `/session/status` | — | the run status of every session, keyed by id. | | GET | `/session/{sessionID}` | `session.get` | — | | DELETE | `/session/{sessionID}` | `session.delete` | Removes the session, its children, message projection and event log. | | PATCH | `/session/{sessionID}` | `session.update` | the v1 title patch. | | POST | `/session/{sessionID}/abort` | `session.abort` | The TS handler is idempotent: it interrupts any running turn and returns `true` even for an unknown session id (no 404 pre-check). Mirrored. The interrupt goes through the runner, not the bare coordinator, so it also reaches the session's sub-agents — foreground through the token cascade, background through the runner's job index (long-runs plan Task 12, L-9). | | GET | `/session/{sessionID}/children` | — | the sub-sessions whose `parentID` is this session. The store has no parent filter, so this lists and filters in-process (the TS handler likewise scans). | | POST | `/session/{sessionID}/command` | — | resolves a registered command, substitutes its arguments, and runs it as a prompt turn. An unknown command maps to `CommandNotFoundError` (404). | | GET | `/session/{sessionID}/diff` | `session.diff` | the structured diff for the session's working directory (git mode). | | POST | `/session/{sessionID}/fork` | — | copies the session into a new one (optionally truncated at `messageID`). | | POST | `/session/{sessionID}/init` | — | runs the project-analysis (`/init`) flow as a prompt turn. | | GET | `/session/{sessionID}/checkpoints` | — | the recorded checkpoints (id, reason, message bookmark, seq), oldest first. An unknown session lists nothing. | | POST | `/session/{sessionID}/undo` | — | restore the last pre-write state. With no earlier checkpoint it is a no-op with a `nothing to undo` body, never an error. | | POST | `/session/{sessionID}/redo` | — | re-apply the next checkpoint's files. Available only after an undo; otherwise a no-op with a `nothing to redo` body. | | GET | `/session/{sessionID}/checkpoints/{a}/diff/{b}` | — | a unified diff between two checkpoints (text/plain); the header names both short ids. An unknown session or checkpoint is a 404. | | POST | `/session/{sessionID}/checkpoints/revert-hunk` | — | restore one diff hunk of `file` to its pre-edit (checkpoint) bytes, leaving every other hunk. An unknown hunk id is a 404 (never a whole-file overwrite). | | POST | `/session/{sessionID}/checkpoints/apply-hunk` | — | re-apply one previously reverted hunk of `file`. | | GET | `/session/{sessionID}/message` | `session.messages` | the v1 alias. | | POST | `/session/{sessionID}/message` | `session.prompt` | the v1 prompt alias. | | GET | `/session/{sessionID}/message/{messageID}` | `session.message` | — | | DELETE | `/session/{sessionID}/message/{messageID}` | `session.deleteMessage` | — | | DELETE | `/session/{sessionID}/message/{messageID}/part/{partID}` | `part.delete` | — | | PATCH | `/session/{sessionID}/message/{messageID}/part/{partID}` | `part.update` | The body is a v1 `Part` (the full tagged-union shape). The flat ported model stores assistant content (text/reasoning/tool) inline rather than as addressable Part rows, so for those types the update is applied to the inline content; for every type the updated Part is returned with the path-derived ids, matching the v1 contract. | | POST | `/session/{sessionID}/permissions/{permissionID}` | — | the v1 session-scoped response. The `{response}` enum maps directly onto `permission::Reply`. | | POST | `/session/{sessionID}/prompt_async` | — | admits durably and returns immediately; the turn runs in the background. | | POST | `/session/{sessionID}/revert` | — | records a revert bookmark at the given message/part. | | POST | `/session/{sessionID}/share` | — | mints (or returns) the share row. | | DELETE | `/session/{sessionID}/share` | — | drops it. | | POST | `/session/{sessionID}/shell` | — | runs a shell command in the session's working directory, recording it as a durable shell message. | | GET | `/session/{sessionID}/subagent` | — | the session's effective sub-agent model (2026-09-16), so a remote TUI shows the identity row's `sub-agents` segment. The engine owns the whole precedence (`ocean_app::Services::effective_subagent_model`); this hands its answer back as `{"model": ModelRef \| null}`. An unknown session is 404, like the sibling per-session GETs. | | POST | `/session/{sessionID}/summarize` | — | runs a summarization turn and reports whether a summary was produced. | | GET | `/session/{sessionID}/todo` | `session.todo` | The `ETag` header is the table's entity tag; send it back as `If-Match` on `PUT` for a conditional edit. | | PUT | `/session/{sessionID}/todo` | — | the user's edit. Rows are matched by id or content; changed and new rows become user-authored; rows left out are removed. With `If-Match` set to a stale tag nothing is written and the answer is `412` carrying the current rows and tag. | | POST | `/session/{sessionID}/unrevert` | — | clears the revert bookmark. | | GET | `/skill` | `app.skills` | the v1 bare array. | ## sync | Method | Path | Operation | What it does | |---|---|---|---| | POST | `/sync/history` | `sync.history.list` | Mirrors `sync.ts history()`: select every durable event from the `event` table, excluding the `(aggregateID, seq)` cursors in the payload, ordered by seq (`sync.ts:72`). The payload maps `aggregateID -> lastSeq` the caller already has. | | POST | `/sync/replay` | `sync.replay` | Mirrors `sync.ts replay()`: replay the serialized events into the event store, return `{sessionID: events[0].aggregateID}` (`sync.ts:34`). TS's `ReplayPayload` declares `directory: Schema.String` and `events: Schema.NonEmptyArray(ReplayEvent)`, so the HttpApi rejects a missing directory or an empty array with a 400 BEFORE the handler runs. | | POST | `/sync/start` | `sync.start` | Mirrors `sync.ts start()`: fork `startWorkspaceSyncing` with `Effect.ignore`, return `true` (`sync.ts:27`). The fork is best-effort and its result is ignored, so the response is unconditionally `true`. | | POST | `/sync/steal` | `sync.steal` | Mirrors `sync.ts steal()`: requires the instance `workspaceID`; without one it returns 400 `BadRequest` (`sync.ts:62`). A self-hosted instance has no workspace binding. The contract's 400 is the effect `HttpApiError BadRequest` (`{_tag:"BadRequest"}`), not the `{name,data}` envelope. | ## tui | Method | Path | Operation | What it does | |---|---|---|---| | POST | `/tui/append-prompt` | `tui.appendPrompt` | — | | POST | `/tui/clear-prompt` | `tui.clearPrompt` | — | | GET | `/tui/control/next` | `tui.control.next` | long-poll for the next queued TUI request (FIFO). Parks until a request is enqueued or the client disconnects. | | POST | `/tui/control/response` | `tui.control.response` | enqueue the TUI's reply for whoever is waiting (fire-and-forget FIFO). | | POST | `/tui/execute-command` | `tui.executeCommand` | — | | POST | `/tui/open-help` | `tui.openHelp` | — | | POST | `/tui/open-models` | `tui.openModels` | — | | POST | `/tui/open-sessions` | `tui.openSessions` | — | | POST | `/tui/open-themes` | `tui.openThemes` | — | | POST | `/tui/publish` | `tui.publish` | dispatch a TUI event onto the request queue for a connected client to pull via `control/next`. | | POST | `/tui/select-session` | `tui.selectSession` | — | | POST | `/tui/show-toast` | `tui.showToast` | — | | POST | `/tui/submit-prompt` | `tui.submitPrompt` | — | ## vcs | Method | Path | Operation | What it does | |---|---|---|---| | GET | `/vcs` | `vcs.get` | current/default branch info. | | POST | `/vcs/apply` | `vcs.apply` | applies a unified patch to the working tree. The contract's 200 body is `{applied: boolean}`. | | GET | `/vcs/diff` | `vcs.diff` | the structured per-file diff. | | GET | `/vcs/diff/raw` | `vcs.diff.raw` | The contract's 200 body is a `text/x-diff` string (not JSON), so the diff is written verbatim with that content type. | | GET | `/vcs/status` | `vcs.status` | the working-tree file status list. | Generated from `crates/ocean-server/src/router.rs` — 218 operations. # Create chat completion Source: https://www.plungeai.com/docs/api-reference/models/create-chat-completion openapi.json post /v1/chat/completions Send messages to a model and get a completion, or stream it as SSE chunks. Ordered fallback with `models[]`, `sort`, presets and an opt-in response cache. # Create message (Anthropic) Source: https://www.plungeai.com/docs/api-reference/models/create-message openapi.json post /v1/messages The Anthropic Messages API on the models plane: point an Anthropic SDK at `https://api.plungeai.com` with your `ozk_` key in `x-api-key`. Any model; `anthropic/*` gets your body as sent, every other model a translation. # Create embeddings Source: https://www.plungeai.com/docs/api-reference/models/create-embeddings openapi.json post /v1/embeddings Turn a string or a list of strings into embedding vectors (OpenAI-compatible). # List models Source: https://www.plungeai.com/docs/api-reference/models/list-models openapi.json get /v1/models The priced model catalog routing candidates are drawn from. `pricing.in` and `pricing.out` are the provider cost basis in USD per 1M tokens, before your org markup. # Price list Source: https://www.plungeai.com/docs/api-reference/models/pricing openapi.json get /v1/pricing The public price list: no key. # Get generation Source: https://www.plungeai.com/docs/api-reference/models/get-generation openapi.json get /v1/generation Post-hoc usage and cost of one completion. id = the gen-… completion id (/api/v1) or the x-request-id. # Get credits Source: https://www.plungeai.com/docs/api-reference/models/get-credits openapi.json get /v1/credits Lifetime credits bought and used by the calling account, in USD. # Get current key Source: https://www.plungeai.com/docs/api-reference/models/get-key openapi.json get /v1/key The calling key: label, monthly limit, usage this month, rate limit. Also served at /v1/auth/key. # List model endpoints Source: https://www.plungeai.com/docs/api-reference/models/list-model-endpoints openapi.json get /v1/models/{author}/{slug}/endpoints The endpoint that serves one model, with per-token pricing (Ocean has one upstream per model). # Create completion Source: https://www.plungeai.com/docs/api-reference/models/create-completion openapi.json post /v1/completions Legacy text completion (proxied to inference-gateway): a chat completion with one user message, answered as text_completion. # Create chat completion (/api/v1) Source: https://www.plungeai.com/docs/api-reference/models/openrouter-create-chat-completion openapi.json post /api/v1/chat/completions Chat completion on the OpenRouter-compatible /api/v1 base. The same engine as POST /v1/chat/completions, with OpenRouter’s error envelope. # Create message (/api/v1) Source: https://www.plungeai.com/docs/api-reference/models/openrouter-create-message openapi.json post /api/v1/messages The Anthropic Messages API on the OpenRouter-compatible /api/v1 base. The same handler as POST /v1/messages, Anthropic’s error envelope included; `fallbacks` takes up to 3 models. # Create embeddings (/api/v1) Source: https://www.plungeai.com/docs/api-reference/models/openrouter-create-embeddings openapi.json post /api/v1/embeddings Embeddings on the OpenRouter-compatible /api/v1 base. # Create completion (/api/v1) Source: https://www.plungeai.com/docs/api-reference/models/openrouter-create-completion openapi.json post /api/v1/completions Legacy text completion on the OpenRouter-compatible /api/v1 base. # List models (/api/v1) Source: https://www.plungeai.com/docs/api-reference/models/openrouter-list-models openapi.json get /api/v1/models The model catalog on the OpenRouter-compatible /api/v1 base. Needs no key. # Get generation (/api/v1) Source: https://www.plungeai.com/docs/api-reference/models/openrouter-get-generation openapi.json get /api/v1/generation Usage and cost of one completion: id is the gen-… completion id. # Get credits (/api/v1) Source: https://www.plungeai.com/docs/api-reference/models/openrouter-get-credits openapi.json get /api/v1/credits Lifetime credits bought and used by the calling account, in USD. # Get current key (/api/v1) Source: https://www.plungeai.com/docs/api-reference/models/openrouter-get-key openapi.json get /api/v1/key The calling key: label, monthly limit, usage this month, rate limit. # List model endpoints (/api/v1) Source: https://www.plungeai.com/docs/api-reference/models/openrouter-list-model-endpoints openapi.json get /api/v1/models/{author}/{slug}/endpoints The endpoint that serves one model, with per-token pricing. # Search the registry Source: https://www.plungeai.com/docs/api-reference/discovery/search-registry openapi.json get /v1/discovery/search Hybrid semantic and keyword search over capability cards. Pages with `limit` and `offset`; a page shorter than `limit` is the last one. # Recommend a card Source: https://www.plungeai.com/docs/api-reference/discovery/recommend-card openapi.json post /v1/discovery/recommend Get the one best capability card for a task, with a score. # Get a card Source: https://www.plungeai.com/docs/api-reference/discovery/get-card openapi.json get /v1/discovery/cards/{type}/{id} Read one capability card as markdown (the LLM view): operations, examples and "Not for" redirects. # Resolve a task Source: https://www.plungeai.com/docs/api-reference/discovery/resolve-task openapi.json post /v1/discovery/resolve Resolve a task to one tool, its operation and an example call, with a calibrated confidence. # List tools Source: https://www.plungeai.com/docs/api-reference/tools/list-tools openapi.json get /v1/tools List the active tool agents, 50 per page by default (at most 100). # Get a tool contract Source: https://www.plungeai.com/docs/api-reference/tools/get-tool-contract openapi.json get /v1/tools/{id} The invocation contract of one tool agent: JSON Schema parameters, operations, approval gates and examples. # Execute a tool Source: https://www.plungeai.com/docs/api-reference/tools/execute-tool openapi.json post /v1/tools/{id}/execute Run one operation of a tool agent with typed params. Gated and money verbs refuse unattended calls. # List agents Source: https://www.plungeai.com/docs/api-reference/agents/list-agents openapi.json get /v1/agents List the active agents, 50 per page by default (at most 100). # List agent categories Source: https://www.plungeai.com/docs/api-reference/agents/list-agent-categories openapi.json get /v1/agents/categories Agent categories with the number of active agents in each. # Execute an agent Source: https://www.plungeai.com/docs/api-reference/agents/execute-agent openapi.json post /v1/agents/{id}/execute Run one agent with a prompt. Sync by default; `sync: false` returns a pointer to redeem later; `stream: true` streams OpenAI-shaped chunks. # Get an agent result Source: https://www.plungeai.com/docs/api-reference/agents/get-agent-result openapi.json get /v1/agents/results/{workflowId}/{taskId} Redeem the result of an async agent run with the `workflow_id` and `task_id` it returned. # Execute an inline workflow Source: https://www.plungeai.com/docs/api-reference/workflows/execute-inline-workflow openapi.json post /v1/workflows/execute Run a CNL workflow sent in the body, as JSON `{workflow, input}` or as a raw `text/yaml` document. Returns a pointer; read the content from the result route. # Stream an inline workflow Source: https://www.plungeai.com/docs/api-reference/workflows/stream-inline-workflow openapi.json post /v1/workflows/execute-stream Run an inline workflow and stream every engine event as SSE until `workflow_completed` or `workflow_error`. # Execute a saved workflow Source: https://www.plungeai.com/docs/api-reference/workflows/execute-saved-workflow openapi.json post /v1/workflows/{id}/execute Run one of your saved workflows by id with an optional `input` or `inputs`. # Stream a saved workflow Source: https://www.plungeai.com/docs/api-reference/workflows/stream-saved-workflow openapi.json post /v1/workflows/{id}/execute-stream Run a saved workflow and stream its engine events as SSE. # Get a workflow result Source: https://www.plungeai.com/docs/api-reference/workflows/get-workflow-result openapi.json get /v1/workflows/results/{workflowId}/{taskId} Read the stored output of one task of a run, usually the `final_task_id` the execute call returned. # Cancel an execution Source: https://www.plungeai.com/docs/api-reference/workflows/cancel-execution openapi.json post /v1/workflows/executions/{id}/cancel Ask a running execution of yours to stop. Cancellation is cooperative. # List workflows Source: https://www.plungeai.com/docs/api-reference/workflows/list-workflows openapi.json get /v1/workflows List your saved workflows, agents, bots and campaigns. # Create a workflow Source: https://www.plungeai.com/docs/api-reference/workflows/create-workflow openapi.json post /v1/workflows Save a workflow, agent, bot or campaign. # Get a workflow Source: https://www.plungeai.com/docs/api-reference/workflows/get-workflow openapi.json get /v1/workflows/{id} Read one saved workflow with its YAML. # Update a workflow Source: https://www.plungeai.com/docs/api-reference/workflows/update-workflow openapi.json patch /v1/workflows/{id} Change a workflow: name, description, YAML, kind or folder. # Delete a workflow Source: https://www.plungeai.com/docs/api-reference/workflows/delete-workflow openapi.json delete /v1/workflows/{id} Delete a workflow. # List versions Source: https://www.plungeai.com/docs/api-reference/workflows/list-workflow-versions openapi.json get /v1/workflows/{id}/versions List the saved versions of a workflow, newest first. # Save a version Source: https://www.plungeai.com/docs/api-reference/workflows/save-workflow-version openapi.json post /v1/workflows/{id}/versions Save the current YAML as a new version. # Restore a version Source: https://www.plungeai.com/docs/api-reference/workflows/restore-workflow-version openapi.json post /v1/workflows/{id}/versions/{versionId}/restore Make a saved version the live YAML again. # List executions Source: https://www.plungeai.com/docs/api-reference/executions/list-executions openapi.json get /v1/executions List your executions, newest first. # Get an execution Source: https://www.plungeai.com/docs/api-reference/executions/get-execution openapi.json get /v1/executions/{id} Status of an execution (poll this). # Delete an execution Source: https://www.plungeai.com/docs/api-reference/executions/delete-execution openapi.json delete /v1/executions/{id} Delete an execution and its stored result. # Get the output Source: https://www.plungeai.com/docs/api-reference/executions/get-execution-output openapi.json get /v1/executions/{id}/output The run’s final output (json, yaml, markdown or text). # Get the conversation Source: https://www.plungeai.com/docs/api-reference/executions/get-execution-conversation openapi.json get /v1/executions/{id}/conversation The follow-up and continue turns of a run. # List the files Source: https://www.plungeai.com/docs/api-reference/executions/list-execution-files openapi.json get /v1/executions/{id}/files Files the run produced. # Continue or approve Source: https://www.plungeai.com/docs/api-reference/executions/continue-execution openapi.json post /v1/executions/{id}/continue Answer a paused run’s question or approve its held action. # Follow up Source: https://www.plungeai.com/docs/api-reference/executions/followup-execution openapi.json post /v1/executions/{id}/followup Message a run: queued while it runs, a new turn once it finished. # Steer a run Source: https://www.plungeai.com/docs/api-reference/executions/steer-execution openapi.json post /v1/executions/{id}/steer Queue a message for a running execution. # Cancel an execution Source: https://www.plungeai.com/docs/api-reference/executions/cancel-execution openapi.json post /v1/executions/{id}/cancel Cancel a running execution (cooperative). # Stream an execution Source: https://www.plungeai.com/docs/api-reference/executions/stream-execution openapi.json get /v1/executions/{id}/stream Re-attach to a running execution (SSE). # Get permissions Source: https://www.plungeai.com/docs/api-reference/bots/get-bot-permissions openapi.json get /v1/workflows/{id}/permissions A bot’s permission presets, admin locks and spend caps. # Set permissions Source: https://www.plungeai.com/docs/api-reference/bots/set-bot-permissions openapi.json put /v1/workflows/{id}/permissions Set a bot’s permission presets, locks and spend caps. # List workspace files Source: https://www.plungeai.com/docs/api-reference/bots/list-workspace-files openapi.json get /v1/workflows/{id}/workspace-files A bot’s persistent workspace files. # Download a workspace file Source: https://www.plungeai.com/docs/api-reference/bots/download-workspace-file openapi.json get /v1/workflows/{id}/workspace-files/download Download one workspace file. # Read the inbox Source: https://www.plungeai.com/docs/api-reference/bots/get-bot-inbox openapi.json get /v1/workflows/{id}/inbox A bot’s inbox. # List bindings Source: https://www.plungeai.com/docs/api-reference/bots/list-bindings openapi.json get /v1/workflows/{id}/bindings Chats bound to a bot. # Bind a chat Source: https://www.plungeai.com/docs/api-reference/bots/bind-chat openapi.json post /v1/workflows/{id}/bindings Bind a paired chat to a bot. # Unbind a chat Source: https://www.plungeai.com/docs/api-reference/bots/unbind-chat openapi.json delete /v1/workflows/{id}/bindings/{channel}/{sender_id} Unbind a chat from a bot. # List bot groups Source: https://www.plungeai.com/docs/api-reference/bots/list-bot-groups openapi.json get /v1/bot-groups Your bot groups. # Create a bot group Source: https://www.plungeai.com/docs/api-reference/bots/create-bot-group openapi.json post /v1/bot-groups Create a bot group. # Replace a bot group Source: https://www.plungeai.com/docs/api-reference/bots/replace-bot-group openapi.json put /v1/bot-groups/{id} Replace a bot group. # Delete a bot group Source: https://www.plungeai.com/docs/api-reference/bots/delete-bot-group openapi.json delete /v1/bot-groups/{id} Delete a bot group. # Read a group thread Source: https://www.plungeai.com/docs/api-reference/bots/get-bot-group-thread openapi.json get /v1/bot-groups/{id}/thread A bot group’s shared thread. # List paired channels Source: https://www.plungeai.com/docs/api-reference/bots/list-channels openapi.json get /v1/channels Your paired channels. # Mint a pairing code Source: https://www.plungeai.com/docs/api-reference/bots/create-pairing-code openapi.json post /v1/channels/pair Mint a pairing code. # List schedules Source: https://www.plungeai.com/docs/api-reference/schedules/list-schedules openapi.json get /v1/schedules List your schedules. # Create a schedule Source: https://www.plungeai.com/docs/api-reference/schedules/create-schedule openapi.json post /v1/schedules Create a schedule of a saved workflow (cron, or one-time with run_at). # Get a schedule Source: https://www.plungeai.com/docs/api-reference/schedules/get-schedule openapi.json get /v1/schedules/{id} Get a schedule with its latest runs. # Update a schedule Source: https://www.plungeai.com/docs/api-reference/schedules/update-schedule openapi.json patch /v1/schedules/{id} Change a schedule. # Delete a schedule Source: https://www.plungeai.com/docs/api-reference/schedules/delete-schedule openapi.json delete /v1/schedules/{id} Delete a schedule. # Run now Source: https://www.plungeai.com/docs/api-reference/schedules/run-schedule openapi.json post /v1/schedules/{id}/run Run a schedule now. # Pause a schedule Source: https://www.plungeai.com/docs/api-reference/schedules/pause-schedule openapi.json post /v1/schedules/{id}/pause Pause a schedule. # Resume a schedule Source: https://www.plungeai.com/docs/api-reference/schedules/resume-schedule openapi.json post /v1/schedules/{id}/resume Resume a paused schedule. # Wake a schedule Source: https://www.plungeai.com/docs/api-reference/schedules/wake-schedule openapi.json post /v1/schedules/{id}/wake Wake the schedule’s workflow once. # List runs Source: https://www.plungeai.com/docs/api-reference/schedules/list-schedule-runs openapi.json get /v1/schedules/{id}/runs List a schedule’s runs. # List webhook triggers Source: https://www.plungeai.com/docs/api-reference/triggers/list-webhook-triggers openapi.json get /v1/triggers/webhook List a schedule's webhook triggers. # Mint a webhook trigger Source: https://www.plungeai.com/docs/api-reference/triggers/create-webhook-trigger openapi.json post /v1/triggers/webhook Mint a webhook trigger on a schedule. # Revoke a webhook trigger Source: https://www.plungeai.com/docs/api-reference/triggers/delete-webhook-trigger openapi.json delete /v1/triggers/webhook/{token} Revoke a webhook trigger. # Rotate a webhook trigger Source: https://www.plungeai.com/docs/api-reference/triggers/rotate-webhook-trigger openapi.json post /v1/triggers/webhook/{token}/rotate Rotate a webhook trigger. # List email triggers Source: https://www.plungeai.com/docs/api-reference/triggers/list-email-triggers openapi.json get /v1/triggers/email List a schedule's email triggers. # Mint an email trigger Source: https://www.plungeai.com/docs/api-reference/triggers/create-email-trigger openapi.json post /v1/triggers/email Mint an email trigger on a schedule. # Revoke an email trigger Source: https://www.plungeai.com/docs/api-reference/triggers/delete-email-trigger openapi.json delete /v1/triggers/email/{token} Revoke an email trigger. # Rotate an email trigger Source: https://www.plungeai.com/docs/api-reference/triggers/rotate-email-trigger openapi.json post /v1/triggers/email/{token}/rotate Rotate an email trigger. # List outbound hooks Source: https://www.plungeai.com/docs/api-reference/triggers/list-hooks openapi.json get /v1/triggers/hooks List a schedule's outbound hooks. # Mint an outbound hook Source: https://www.plungeai.com/docs/api-reference/triggers/create-hook openapi.json post /v1/triggers/hooks Mint an outbound hook on a schedule. # Revoke an outbound hook Source: https://www.plungeai.com/docs/api-reference/triggers/delete-hook openapi.json delete /v1/triggers/hooks/{id} Revoke an outbound hook. # Recall memory Source: https://www.plungeai.com/docs/api-reference/memory/recall-memory openapi.json get /v1/memory/recall Recall your long-term memory entries (optionally filtered by text). # Remember a fact Source: https://www.plungeai.com/docs/api-reference/memory/remember openapi.json post /v1/memory/remember Remember a fact. # Forget Source: https://www.plungeai.com/docs/api-reference/memory/forget openapi.json post /v1/memory/forget Forget one entry by id, or purge every entry containing some text. # Search past runs Source: https://www.plungeai.com/docs/api-reference/memory/search-runs openapi.json get /v1/memory/runs Search your past runs. # Get a past run Source: https://www.plungeai.com/docs/api-reference/memory/get-run openapi.json get /v1/memory/runs/{id} One past run’s stored result. # List skills Source: https://www.plungeai.com/docs/api-reference/skills/list-skills openapi.json get /v1/skills List your learned skills. # Save a skill Source: https://www.plungeai.com/docs/api-reference/skills/learn-skill openapi.json post /v1/skills/learn Save a skill. # Delete a skill Source: https://www.plungeai.com/docs/api-reference/skills/delete-skill openapi.json delete /v1/skills/{id} Delete one of your skills. # List templates Source: https://www.plungeai.com/docs/api-reference/templates/list-templates openapi.json get /v1/templates Browse the workflow, agent and bot templates. # Get a template Source: https://www.plungeai.com/docs/api-reference/templates/get-template openapi.json get /v1/templates/{id} One template and its YAML. # Use a template Source: https://www.plungeai.com/docs/api-reference/templates/use-template openapi.json post /v1/templates/{id}/use Copy a template into your account as a new workflow. # List connections Source: https://www.plungeai.com/docs/api-reference/connections/list-connections openapi.json get /v1/connections List your connections, or one customer’s (status only). # Connect a provider Source: https://www.plungeai.com/docs/api-reference/connections/connect-provider openapi.json post /v1/connections/{provider} Bring a key for a provider (yours, or your customer’s). # Disconnect a provider Source: https://www.plungeai.com/docs/api-reference/connections/disconnect-provider openapi.json delete /v1/connections/{provider} Disconnect a provider (yours, or your customer’s). # Mint a connect link Source: https://www.plungeai.com/docs/api-reference/connections/create-connect-link openapi.json post /v1/connections/{provider}/link Get a hosted connect link to send to your customer. # List companies Source: https://www.plungeai.com/docs/api-reference/companies/list-companies openapi.json get /v1/companies Your applied campuses: mode, status, building count and the approvals waiting at each door. # Apply a company Source: https://www.plungeai.com/docs/api-reference/companies/apply-company openapi.json post /v1/companies/{campus}/apply Compile the saved campus, building and office documents into the stored plan; inline offices are saved as workflows. # Get the layout Source: https://www.plungeai.com/docs/api-reference/companies/get-company-layout openapi.json get /v1/companies/{campus}/layout The compiled company: buildings with policy, floors, intake and pipeline, and their offices. # Get the snapshot Source: https://www.plungeai.com/docs/api-reference/companies/get-company-snapshot openapi.json get /v1/companies/{campus}/snapshot The company now: office states, active items, the line at the door, spend today and items shipped in 7 days. # List events Source: https://www.plungeai.com/docs/api-reference/companies/list-company-events openapi.json get /v1/companies/{campus}/events The event log as a JSON page after a seq, or a live SSE feed. # Report events Source: https://www.plungeai.com/docs/api-reference/companies/report-company-events openapi.json post /v1/companies/{campus}/events A local building’s node mirrors its own events up with the owner’s key. # Get a building slice Source: https://www.plungeai.com/docs/api-reference/companies/get-building-slice openapi.json get /v1/companies/{campus}/buildings/{building}/slice What a local building’s node pulls to run it: pipeline, policy, project and office ids. # Create an item Source: https://www.plungeai.com/docs/api-reference/companies/create-company-item openapi.json post /v1/companies/{campus}/items Put an item into a building at its intake. # Read an item Source: https://www.plungeai.com/docs/api-reference/companies/get-company-item openapi.json get /v1/companies/{campus}/items/{id} One item in full: its bodies and every run, approval and own event. The snapshot carries only the lean row. # Pause an item Source: https://www.plungeai.com/docs/api-reference/companies/pause-company-item openapi.json post /v1/companies/{campus}/items/{id}/pause The item keeps its step and is not walked until it is resumed. # Resume an item Source: https://www.plungeai.com/docs/api-reference/companies/resume-company-item openapi.json post /v1/companies/{campus}/items/{id}/resume Resume a paused or parked item at the step it stopped on. # Retry an item Source: https://www.plungeai.com/docs/api-reference/companies/retry-company-item openapi.json post /v1/companies/{campus}/items/{id}/retry Start the item’s step again as the next attempt, or at the pipeline’s retry target. # Cancel an item Source: https://www.plungeai.com/docs/api-reference/companies/cancel-company-item openapi.json post /v1/companies/{campus}/items/{id}/cancel End the item failed with parked_reason cancelled. # Steer an item Source: https://www.plungeai.com/docs/api-reference/companies/steer-company-item openapi.json post /v1/companies/{campus}/items/{id}/steer A note the next office run receives once, appended to its input. # List approvals Source: https://www.plungeai.com/docs/api-reference/companies/list-company-approvals openapi.json get /v1/companies/{campus}/approvals The gates waiting on a person and the tool permissions office runs asked for. # Decide an approval Source: https://www.plungeai.com/docs/api-reference/companies/decide-company-approval openapi.json post /v1/companies/{campus}/approvals/{ap} Approve or reject a pending gate or tool permission. # Set the mode Source: https://www.plungeai.com/docs/api-reference/companies/set-company-mode openapi.json put /v1/companies/{campus}/mode Switch the company live between supervised, tiered and autonomous. # Start the clock Source: https://www.plungeai.com/docs/api-reference/companies/start-company openapi.json post /v1/companies/{campus}/start Arm the campus clock: its items are walked until it is stopped. # Stop the clock Source: https://www.plungeai.com/docs/api-reference/companies/stop-company openapi.json post /v1/companies/{campus}/stop Disarm the campus clock; items keep their step and wait. # Delete a company Source: https://www.plungeai.com/docs/api-reference/companies/delete-company openapi.json delete /v1/companies/{campus} Delete a stopped or never-started company and every row of it. # Agent Call Source: https://www.plungeai.com/docs/api-reference/agent-call/agent-call openapi.json post /v1/execute Agent Call — a bare building block, a workflow, or a saved agent (agent: <id>[@n]) in one body. # List MCP tools Source: https://www.plungeai.com/docs/api-reference/mcp/list-mcp-tools openapi.json get /v1/mcp/tools The MCP server tool list as a plain GET; the answer is the JSON-RPC `tools/list` envelope with the tools under `result.tools`. # MCP endpoint (JSON-RPC) Source: https://www.plungeai.com/docs/api-reference/mcp/mcp-endpoint openapi.json post /v1/mcp The Streamable HTTP MCP endpoint: `initialize`, then `tools/list` and `tools/call`. The samples use the MCP host `https://mcp.plungeai.com/v1`, the same server. # Open an MCP run Source: https://www.plungeai.com/docs/api-reference/mcp/create-mcp-run openapi.json post /v1/mcp/runs Connect catalog MCP servers for a run and get their namespaced tool table. Always close the run when done. # List MCP run tools Source: https://www.plungeai.com/docs/api-reference/mcp/list-mcp-run-tools openapi.json get /v1/mcp/runs/{id}/tools List the namespaced tools (`mcp__<server>__<tool>`) of an open MCP run. # Call an MCP run tool Source: https://www.plungeai.com/docs/api-reference/mcp/call-mcp-run-tool openapi.json post /v1/mcp/runs/{id}/call Call one namespaced tool of an open MCP run with its arguments. # Close an MCP run Source: https://www.plungeai.com/docs/api-reference/mcp/close-mcp-run openapi.json delete /v1/mcp/runs/{id} Close an MCP run and drop its state. Idempotent. # Get a trace Source: https://www.plungeai.com/docs/api-reference/traces/get-trace openapi.json get /v1/traces/{id} The persisted trace of one run: execution spans and the gateway request log for the trace id. # Health Source: https://www.plungeai.com/docs/api-reference/meta/health openapi.json get /health Liveness probe of the One API router. No key needed. # OpenAPI spec Source: https://www.plungeai.com/docs/api-reference/meta/get-openapi-spec openapi.json get /v1/openapi.json The OpenAPI 3.1 document of the One API. The docs copy with tags is at [/openapi.json](/openapi.json). # llms.txt Source: https://www.plungeai.com/docs/api-reference/meta/llms-txt <Note> Not in the OpenAPI spec; documented from the route code. </Note> ```text Endpoint GET https://api.plungeai.com/llms.txt ``` `GET https://api.plungeai.com/llms.txt` returns the index of the One API Developer Guide in the [llmstxt.org](https://llmstxt.org) format, so an AI coding agent can onboard from the domain alone. No key is needed. ## Response <ResponseField name="200" type="text/plain"> The guide index. Cached for 5 minutes (`Cache-Control: public, max-age=300`). </ResponseField> ## Example <CodeGroup> ```python Python import requests print(requests.get('https://api.plungeai.com/llms.txt').text) ``` ```typescript TypeScript const res = await fetch('https://api.plungeai.com/llms.txt') console.log(await res.text()) ``` ```java Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.plungeai.com/llms.txt")).GET().build(); HttpResponse<String> res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()); System.out.println(res.body()); } } ``` ```bash cURL curl -s https://api.plungeai.com/llms.txt ``` </CodeGroup> The index of this documentation site is [/llms.txt](/llms.txt) under www.plungeai.com/docs. # llms-full.txt Source: https://www.plungeai.com/docs/api-reference/meta/llms-full-txt <Note> Not in the OpenAPI spec; documented from the route code. </Note> ```text Endpoint GET https://api.plungeai.com/llms-full.txt ``` `GET https://api.plungeai.com/llms-full.txt` returns the full One API Developer Guide 3.1 as one markdown document, for agents that read everything at once. No key is needed. ## Response <ResponseField name="200" type="text/markdown; charset=utf-8"> The full guide. Cached for 5 minutes (`Cache-Control: public, max-age=300`). </ResponseField> ## Example <CodeGroup> ```python Python import requests open('one-api-guide.md', 'w').write(requests.get('https://api.plungeai.com/llms-full.txt').text) ``` ```typescript TypeScript const res = await fetch('https://api.plungeai.com/llms-full.txt') console.log((await res.text()).length) ``` ```java Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Path; public class Main { public static void main(String[] args) throws Exception { HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.plungeai.com/llms-full.txt")).GET().build(); HttpResponse<Path> res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofFile(Path.of("one-api-guide.md"))); System.out.println(res.statusCode() + " " + res.body()); } } ``` ```bash cURL curl -s https://api.plungeai.com/llms-full.txt -o one-api-guide.md ``` </CodeGroup> The full text of this documentation site is [/llms-full.txt](/llms-full.txt) under www.plungeai.com/docs. # Guide (HTML) Source: https://www.plungeai.com/docs/api-reference/meta/docs <Note> Not in the OpenAPI spec; documented from the route code. </Note> ```text Endpoint GET https://api.plungeai.com/docs ``` `GET https://api.plungeai.com/docs` answers `301 Moved Permanently` with `Location: https://www.plungeai.com/docs/one-api` (the query string kept). The One API Developer Guide is published there, one page per chapter, as part of these docs: start at the [One API guide](/one-api). No key is needed. An old link to a section (`https://api.plungeai.com/docs#<id>`) keeps its fragment through the redirect, and the guide page moves on to the chapter page that holds that section. The guide as one Markdown file is [`GET /llms-full.txt`](/api-reference/meta/llms-full-txt) on the API host. ## Response <ResponseField name="301" type="redirect"> `Location: https://www.plungeai.com/docs/one-api`, cached for an hour (`Cache-Control: public, max-age=3600`). </ResponseField> ## Example <CodeGroup> ```python Python import requests r = requests.get('https://api.plungeai.com/docs', allow_redirects=False) print(r.status_code, r.headers['location']) ``` ```typescript TypeScript const res = await fetch('https://api.plungeai.com/docs', { redirect: 'manual' }) console.log(res.status, res.headers.get('location')) ``` ```java Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Main { public static void main(String[] args) throws Exception { HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.plungeai.com/docs")).GET().build(); HttpResponse<Void> res = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build().send(req, HttpResponse.BodyHandlers.discarding()); System.out.println(res.statusCode() + " " + res.headers().firstValue("location").orElse("")); } } ``` ```bash cURL curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://api.plungeai.com/docs ``` </CodeGroup> # Who am I Source: https://www.plungeai.com/docs/api-reference/meta/get-whoami openapi.json get /v1/whoami The identity behind the calling ozk_ key: user id, tier and key label. # Four wires Source: https://www.plungeai.com/docs/api-reference/four-wires The One API returns one payload and lets you pick the wire it travels on. The execute routes (tools, agents, workflows and the agent call door) and the bot routes under `/v1/workflows`, `/v1/schedules` and `/v1/executions` all share one format rule, so a script, a shell pipeline and an LLM can each read the same answer in the shape that suits them. | Wire | Content type | You get | |---|---|---| | `json` | `application/json` | The envelope. The default. | | `yaml` | `text/yaml` | The same envelope. Markdown fields travel as block scalars. | | `markdown` | `text/markdown` | The result rendered by the same renderer the [MCP server](/developer-tools/mcp/quickstart) uses, with the execution id in a footer. | | `text` | `text/plain` | The bare result content, nothing else. | ## Choose the wire The first rule that applies wins: 1. `format` in the body, or `?format=` in the query. 2. The `Accept` header. The highest `q` value that names a known type wins; ties go to the first listed. `*/*` and unknown types are skipped, and an `Accept` is a preference, never a `400`. 3. The request's `Content-Type`: `text/yaml`, `application/yaml` or `application/x-yaml` answers in YAML, anything else in JSON. | `Accept` | Wire | |---|---| | `application/json` | `json` | | `text/yaml`, `application/yaml`, `application/x-yaml` | `yaml` | | `text/markdown` | `markdown` | | `text/plain` | `text` | An explicit `format` that is not one of `json`, `yaml`, `markdown` or `text` is `400 invalid_format`, answered as JSON. Streams (`stream: true`, `execute-stream`) are exempt and always send server-sent events. ## Send JSON or YAML A request body is JSON or YAML, whichever its `Content-Type` says. Tool fields can be sent with or without the `params` wrapper. The same call on the tools door, once with JSON and once with YAML: ```bash curl -s -X POST https://api.plungeai.com/v1/tools/calculator-tool-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"params":{"expression":"7*6"}}' ``` ```bash curl -s -X POST https://api.plungeai.com/v1/tools/calculator-tool-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' \ --data-binary $'expression: "7*6"\n' ``` On a workflow route, `format`, `input` and `inputs` sit at the top of the YAML next to `workflow:`. They mean exactly what the JSON fields mean and are removed before the engine sees the workflow: ```yaml format: markdown workflow: name: w tasks: - id: t1 type: task agent: calculator-tool-agent expression: "7*6" ``` A bare `agent:` body works on the agent call door, `POST /v1/execute`: `agent: calculator-tool-agent`, the agent's own fields and `format: text`. See [Agent call](/api-reference/agent-call/agent-call). ## Read it on four wires The same run, asked four ways: <CodeGroup> ```bash json curl -s -X POST https://api.plungeai.com/v1/tools/calculator-tool-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"params":{"expression":"7*6"}}' ``` ```bash yaml curl -s -X POST https://api.plungeai.com/v1/tools/calculator-tool-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -H 'Accept: text/yaml' -d '{"params":{"expression":"7*6"}}' ``` ```bash markdown curl -s -X POST https://api.plungeai.com/v1/tools/calculator-tool-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -H 'Accept: text/markdown' -d '{"params":{"expression":"7*6"}}' ``` ```bash text curl -s -X POST "https://api.plungeai.com/v1/tools/calculator-tool-agent/execute?format=text" \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -d '{"params":{"expression":"7*6"}}' ``` </CodeGroup> The `json` answer: ```json {"ok":true,"content":"# Calculator: 7*6\n\n## 1. Expression\n7*6\n\n## 2. Result\n42\n\n*2 results*","execution_id":"…","request_id":"…"} ``` The `yaml` answer carries the same fields: ```yaml ok: true content: |- # Calculator: 7*6 ## 1. Expression 7*6 ## 2. Result 42 *2 results* execution_id: … request_id: … ``` The `markdown` answer is the content followed by a footer naming the execution, so a reader can quote the id: ```markdown # Calculator: 7*6 ## 1. Expression 7*6 ## 2. Result 42 *2 results* --- execution: `…` — use this id for plungeai_get_result / plungeai_followup on this run. ``` The `text` answer is only the content, from `# Calculator: 7*6` to `*2 results*`. A route that answers with a pointer instead of a result (an async run) has no `content`. Its `markdown` and `text` wires then list the pointer's top-level keys (`workflow_id`, `execution_id`, `task_id`, `status`), one per line, so nothing is dropped. ## Response headers - `x-request-id` is on every answer, on every wire. - `X-Execution-Id` is on every answer that belongs to an execution. - `X-Error-Code` is on every error. ## Errors on four wires An error keeps its `code` and `message` on every wire. The extra fields travel too: `missing`, `contract`, `hint` and the execution id render on `markdown`, `missing` on `text`, and every field survives `json` and `yaml`. A missing field, asked as text: ```bash curl -s -X POST https://api.plungeai.com/v1/tools/calculator-tool-agent/execute \ -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \ -H 'Accept: text/plain' -d '{"params":{}}' ``` ```text invalid_params: params failed card validation missing: expression ``` An unknown `format` is always JSON, with `X-Error-Code: invalid_format`: ```json {"error":{"code":"invalid_format","message":"format must be one of: json, yaml, markdown, text"}} ``` ## MCP and the CLI The MCP tools answer `markdown` by default. Pass `format: "json"` for the same outcome as a JSON document in `text` plus `structuredContent`. The CLI takes `--json` on every verb for one JSON document on stdout. ## Next steps <CardGroup cols={2}> <Card title="Errors, limits and headers" icon="book" href="/getting-started/rate-limits"> Rate limits and the shared error shape. </Card> <Card title="Control a bot" icon="robot" href="/bots/rest-mcp-cli"> The bot routes on all three doors. </Card> </CardGroup> # MCP server overview Source: https://www.plungeai.com/docs/mcp-reference/overview The PlungeAI MCP server exposes 22 `plungeai_*` tools, one prompt, five resources and three resource templates to any MCP client. This tab documents every tool, resource and protocol method: the tool, resource and prompt pages are generated from the server code; this overview and the protocol pages are written from the MCP guide. ## Endpoint | Item | Value | |---|---| | URL | `https://mcp.plungeai.com/v1` (`/v1/` answers the same) | | Legacy path | `/mcp` on the same host still answers with the same handler; configure `/v1` | | Methods | `POST` with JSON-RPC 2.0. `GET` and `DELETE` without a session answer `405` | | Request headers | `Authorization: Bearer $PLUNGE_API_KEY` or `X-API-Key: $PLUNGE_API_KEY`, `Content-Type: application/json`, `Accept: application/json, text/event-stream` | | Responses | `application/json`; `tools/call` answers as `text/event-stream`; a notification alone answers `202` with no body | <Note> The docs site's own MCP server (`www.plungeai.com/docs/mcp`, planned as TI-35) is a different server. Its resources use the `plungeai-docs://` scheme, so they never collide with this server's `plungeai://` URIs. </Note> ## Versions The server version is `2.7.0` and it implements MCP revision `2025-11-25`. It negotiates down to `2024-11-05`: a client that sends `2025-06-18`, `2025-03-26`, `2024-11-05` or `2024-10-07` in `initialize` gets that version back, and any other value gets `2025-11-25`. After `initialize` you may send `MCP-Protocol-Version`; an unsupported value answers `400`, and leaving the header out is fine. See [initialize](/mcp-reference/protocol/initialize). <!-- SERVER_VERSION and PROTOCOL_VERSION: orchestration/mcp-gateway/server.ts:75-80. --> ## Authentication and key fences Every request needs an `ozk_` key, `tools/list` included; create one at **Dashboard → One API → Keys**. Key fences are set by PlungeAI on request. A key fenced to some tools does not see the others in `tools/list`, and a `tools/call` to one of them answers JSON-RPC `-32602` `Tool not permitted for this key: <name>`. Check your identity and tier with [plungeai_whoami](/mcp-reference/tools/plungeai_whoami). ## Stateless requests and sessions Each `POST` gets a fresh server instance and stands alone: no `Mcp-Session-Id` is issued and none is needed. Because nothing is kept between requests, `notifications/tools/list_changed` is never sent, `logging/setLevel` is not persisted and a dropped `tools/call` stream is not replayed (the run keeps going; read it with `plungeai_get_result`). Stateful sessions exist only on keys PlungeAI issues with sessions enabled: `initialize` then returns an `Mcp-Session-Id` header to send on every later request, and a session expires after 10 minutes without a request. ## Batches A JSON array of messages is answered as an array, one answer per message: ```bash curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '[{"jsonrpc":"2.0","id":1,"method":"ping"},{"jsonrpc":"2.0","id":2,"method":"prompts/list"}]' ``` A batch that contains `initialize` with other messages answers `400 -32600`, and a batch with a `tools/call` is answered as SSE. ## Ignored and rejected methods | Method | What happens | |---|---| | `notifications/cancelled` | Ignored: `202`. To stop a run, call `plungeai_executions` with `{"action":"cancel"}` | | `resources/subscribe`, `resources/unsubscribe`, `completion/complete` | Rejected: `-32601 Method not found` | | Any other method | Rejected: `-32601 Method not found` | The server never sends requests to your client: no sampling, elicitation or roots. ## Errors Protocol errors mean the request never reached a tool. The body is always `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 400 | `-32700` | The body is not valid JSON, or not a JSON-RPC message | | 400 | `-32600` | A batch holding `initialize` plus other messages | | 400 | `-32000` | An unsupported `MCP-Protocol-Version` header after `initialize` | | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | | 404 | `-32001` | An `Mcp-Session-Id` that is unknown or expired | | 405 | `-32000` | `GET` or `DELETE` without a session | | 413 | `-32600` | A body over 1 MiB | | 415 | `-32000` | A complete `Accept` header with a non-JSON `Content-Type` | | 429 | `-32000` | Rate limit reached; `Retry-After` and `error.data.retryAfter` give the seconds | | 200 | `-32602` | Unknown tool, a tool your key is fenced from, malformed `initialize` params, or an unknown prompt | | 200 | `-32002` | `resources/read` for an unknown resource, workflow or execution | | 200 | `-32601` | A method the server does not implement | | 500 | `-32603` | An unhandled server error | Tool failures are not protocol errors: execution tools answer with an outcome (`ok`, `needs_input`, `needs_connection`, `needs_api_key`, `needs_approval`, `unavailable` or `error`) inside a normal result. ## Helper routes | Route | Key | Returns | |---|---|---| | `GET https://mcp.plungeai.com/v1/info` | no | Server name `plungeai.com`, version, protocol version and capabilities | | `GET https://mcp.plungeai.com/health` | no | Dependency probes; `200` when healthy, `503` when one fails | ## Recurring parameters Two arguments appear on many tool pages and mean the same thing everywhere: | Argument | On | Meaning | |---|---|---| | `user_request` | Every tool | The user's request in their own words, up to 4,000 characters, optional. Always send it: the platform uses it for context and routing | | `format` | Nine tools | `markdown` (the default) or `json`; `json` returns the same answer as a JSON document and as `structuredContent` | # plungeai_execute_workflow Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_execute_workflow Execute a CNL workflow (saved workflow_id or ad-hoc workflow_yaml). Streams progress. Use mode:"async" for long runs (>3 min) — it returns an execution_id to poll with plungeai_get_workflow_status. In ad-hoc YAML, write long strings as block scalars (`prompt: |`) — never hard-wrap a value; structured tool-agents take their card's parameters as task fields alongside `agent:`. Always answers with a structured outcome (never a raw error) — follow the remediation instead of retrying the identical call. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `workflow_yaml` | string | no | — | Ad-hoc CNL workflow YAML. The `workflow:` wrapper is optional — bare top-level name/tasks is accepted and wrapped automatically, and a bare agent call (`agent: <id>` plus the agent fields) lifts to a one-task workflow. `format`, `input`, `inputs`, `stream`, `sync`, `dry_run`, `metadata` are wire keys and never reach the agent. | | `workflow_id` | string | no | — | | | `input` | string | no | — | | | `inputs` | object | no | — | | | `mode` | enum<string> | no | sync | | | `user_context` | object | no | the request's network location | Where and when the end user is. Optional: the agent uses it for local time and as the default schedule time zone; when absent the gateway derives a time zone and approximate place from the request's network location. Values that do not validate (an unknown zone, a malformed locale) are dropped. | | `format` | enum<string> | no | markdown | markdown (default): rendered for an AI reader · json: the same outcome as a JSON document in `text` plus `structuredContent` | # plungeai_execute_agent Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_execute_agent Execute a single PROMPT-DRIVEN agent, e.g. "one-agent" (call mode; add a persona for a specialist answer). Pass session_id to start/continue a conversational agent. Structured tool-agents (cards with a Parameters table, e.g. markitdown, weather-agent) take typed fields, not prose — use plungeai_get_tool_contract + plungeai_execute_tool for those. Always answers with a structured outcome (never a raw error) — follow the remediation instead of retrying the identical call. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `agent` | string | yes | — | | | `prompt` | string | yes | — | | | `persona` | string | no | — | | | `model` | string | no | the agent's default | | | `provider` | string | no | the model's catalog provider | | | `maxTokens` | integer | no | — | | | `max_tokens` | integer | no | — | | | `temperature` | number | no | — | | | `top_p` | number | no | — | | | `reasoning_effort` | enum<string> | no | — | | | `thinking_level` | string | no | — | | | `streaming` | boolean | no | true | | | `session_id` | string | no | — | | | `user_context` | object | no | the request's network location | Where and when the end user is. Optional: the agent uses it for local time and as the default schedule time zone; when absent the gateway derives a time zone and approximate place from the request's network location. Values that do not validate (an unknown zone, a malformed locale) are dropped. | | `format` | enum<string> | no | markdown | markdown (default): rendered for an AI reader · json: the same outcome as a JSON document in `text` plus `structuredContent` | # plungeai_get_tool_contract Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_get_tool_contract The exact invocation contract for one registry agent: JSON Schema for its parameters, operations (with approval gates), worked YAML examples, output shape, and LIVE credential status for the acting user (platform-managed vs "connect Google first"). Fetch this before the first plungeai_execute_tool call to an unfamiliar agent — the contract IS the API docs. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `agent_id` | string | yes | — | | | `format` | enum<string> | no | markdown | markdown (default): rendered for an AI reader · json: the same outcome as a JSON document in `text` plus `structuredContent` | # plungeai_execute_tool Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_execute_tool Execute one structured tool-agent directly with typed params: {agent_id, operation?, params}. Runs through the engine with full observability; use mode:"async" for long runs. Always answers with a structured outcome (never a raw error): ok → result; needs_input → missing fields + the schema; needs_connection/needs_api_key → tell the user exactly what to connect in our apps, then retry; needs_approval → relay the ⏸ block and use plungeai_continue after the user decides; unavailable → real alternatives. Follow the remediation — do not retry the identical call blind. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `agent_id` | string | yes | — | | | `operation` | string | no | the card's default operation | | | `params` | object | no | {} | | | `prompt` | string | no | — | | | `mode` | enum<string> | no | sync | | | `user_context` | object | no | the request's network location | Where and when the end user is. Optional: the agent uses it for local time and as the default schedule time zone; when absent the gateway derives a time zone and approximate place from the request's network location. Values that do not validate (an unknown zone, a malformed locale) are dropped. | | `format` | enum<string> | no | markdown | markdown (default): rendered for an AI reader · json: the same outcome as a JSON document in `text` plus `structuredContent` | # plungeai_run_mission Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_run_mission Run a bounded autonomous agent mission (loop agent): give a goal, optional tool fence and iteration cap. Recalls and writes long-term memory automatically. Defaults to async — poll plungeai_get_workflow_status. Always answers with a structured outcome (never a raw error) — follow the remediation instead of retrying the identical call. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `goal` | string | yes | — | | | `agent` | string | no | one-agent | The loop runtime the mission runs on. Defaults to one-agent. | | `mission` | string | no | You are a capable autonomous agent. Achieve the goal thoroughly and cite sources. | | | `allowed_tools` | string[] | no | ["web_search", "web_fetch", "task_complete"] | | | `max_iterations` | integer | no | 8 | | | `success_criteria` | string[] | no | — | | | `persona` | string | no | — | | | `skills` | string[] | no | — | | | `mode` | enum<string> | no | async | | | `user_context` | object | no | the request's network location | Where and when the end user is. Optional: the agent uses it for local time and as the default schedule time zone; when absent the gateway derives a time zone and approximate place from the request's network location. Values that do not validate (an unknown zone, a malformed locale) are dropped. | # plungeai_learn Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_learn Manage your private skill library. `action: learn` (default) distills a URL, PDF text, or a past workflow into a reusable skill via the `learn` pre-built agent (needs `source`; defaults to async — poll plungeai_get_workflow_status). `action: list` returns your learned skills (id + summary). `action: forget` deletes one by `name` (the id from list). Always answers with a structured outcome (never a raw error) — follow the remediation instead of retrying the identical call. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `action` | enum<string> | no | learn | learn (default): distill source into a skill · list: your learned skills · forget: delete one (name required) | | `source` | string | no | — | A URL, or text/markdown to distill, or a description of what you just did. | | `name` | string | no | picked automatically on learn | Skill id (kebab-case): picked automatically on learn, required on forget. | | `mode` | enum<string> | no | async | | # plungeai_list_agents Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_list_agents Discover platform building-block agents from the live registry (the capability catalog). NOT the user's own agents — for "show me my agents/workflows" use plungeai_list_workflows. `search` is a hybrid semantic+keyword query — describe the capability in natural language (e.g. "web search", "pdf to markdown"). Prefer search/category filters; an unfiltered list returns the whole catalog (~90 agents, large). The catalog is active-only, and execution refuses any id that is not active — so take ids from here, not from memory. Fetch one full card with agent_id. With `format: "json"` the result is `{content: <markdown>}` — its source is rendered registry markdown; typed rows are a follow-up. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `search` | string | no | — | | | `category` | string | no | — | | | `agent_id` | string | no | — | | | `kind` | enum<string> | no | agents | | | `limit` | integer | no | 25 with search or category, 100 without | | | `format` | enum<string> | no | markdown | markdown (default): rendered for an AI reader · json: the same outcome as a JSON document in `text` plus `structuredContent` | # plungeai_list_workflows Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_list_workflows List your saved workflows — what users usually mean by "my agents". No args shows your folders (rubrics) + recent; filter with folder, search and/or kind. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `search` | string | no | — | | | `folder` | string | no | — | | | `kind` | enum<string> | no | — | | | `limit` | integer | no | 50 | | | `format` | enum<string> | no | markdown | markdown (default): rendered for an AI reader · json: the same outcome as a JSON document in `text` plus `structuredContent` | # plungeai_get_result Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_get_result Retrieve the result of one of your executions (by `execution_id`) — for conversational runs this includes the FULL conversation thread (all follow-up turns), same as Studio shows. Pass task_id to read a single task's output (e.g. one branch of a workflow) instead of the final result. The output is final user-ready markdown: show it to the user verbatim and complete — never summarize or re-type it. With `format: "json"` the result is the `GET /v1/executions/{id}/output` body — `execution_id`, `status`, `content` (the stored output), `final_task_id`, `steps`, `continuation` — not the rendered thread. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `execution_id` | string | no | — | | | `workflow_id` | string | no | — | | | `task_id` | string | no | the final task | | | `format` | enum<string> | no | markdown | markdown (default): rendered for an AI reader · json: the same outcome as a JSON document in `text` plus `structuredContent` | # plungeai_get_workflow_status Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_get_workflow_status Check the status of one of your executions (self-heals stuck runs). Returns structured content too, including `continuation` when the run is paused awaiting user approval or an answer — relay that to the user and use plungeai_continue. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `execution_id` | string | yes | — | | # plungeai_workflow Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_workflow Manage your workflows: action create|get|update|delete|save_version|list_versions|get_version|restore_version|a2a_door (door_action mint|get|pause|resume|revoke — the workflow's Agent2Agent door). kind workflow|agent|bot|campaign (a campaign create takes campaign_config and sets up its ledger and schedule). Bot control on a saved bot (workflow_id): permissions_get|permissions_set (classes, locks, budget_usd_run, budget_usd_month), files|file (path; ≤ 5 MB, base64)|inbox, bindings|bind|unbind (channel, sender_id); bot groups by name: groups|group_set (group, members)|group_delete|group_thread. Create/update/delete sync live to Studio, iMini, and peer apps. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `action` | enum<string> | yes | — | | | `workflow_id` | string | no | — | | | `name` | string | no | — | | | `yaml` | string | no | — | | | `description` | string | no | — | | | `folder` | string | no | — | | | `kind` | enum<string> | no | workflow on create | | | `campaign_config` | object | no | — | | | `version_id` | string | no | — | | | `version_description` | string | no | — | | | `door_action` | enum<string> | no | — | | | `handle` | string | no | the workflow's name as a slug | | | `classes` | object | no | — | | | `locks` | string[] | no | — | | | `budget_usd_run` | number \| null | no | — | | | `budget_usd_month` | number \| null | no | — | | | `path` | string | no | — | | | `channel` | string | no | — | | | `sender_id` | string | no | — | | | `group` | string | no | — | | | `members` | string[] | no | — | | # plungeai_executions Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_executions Browse your execution history: action list|get|output|conversation|files|delete|cancel|steer. `cancel` stops a running execution (cooperative — it stops at its next turn or tool boundary); `steer` (message) queues a message for a running one, delivered at its next turn; `files` lists the files the run wrote. Outputs are final user-ready markdown (tables, threads): show them to the user verbatim and complete — never summarize or re-type them. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `action` | enum<string> | yes | — | | | `execution_id` | string | no | — | | | `workflow_id` | string | no | — | | | `message` | string | no | — | | | `limit` | integer | no | 20 | | | `offset` | integer | no | 0 | | | `format` | enum<string> | no | markdown | markdown (default): rendered for an AI reader · json: the same outcome as a JSON document in `text` plus `structuredContent` | # plungeai_followup Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_followup Ask a follow-up about a completed execution (continues its agent session or researches with prior context). If the execution is still running, your message is queued and injected at its next turn (steer); if finished, it continues the conversation. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `execution_id` | string | yes | — | | | `prompt` | string | yes | — | | # plungeai_continue Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_continue Continue a paused conversation: answer a question (message) or approve a pending action (approve:true). Only pass approve:true after the user explicitly confirmed — never approve on your own. To deny or change course, pass their words as message. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `execution_id` | string | yes | — | | | `message` | string | no | — | | | `approve` | boolean | no | — | | # plungeai_chat Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_chat Persistent chat with the PlungeAI assistant (platform tools + web search). action send|new|list_sessions|history. Conversations appear in Studio and the CLI. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `action` | enum<string> | yes | — | | | `message` | string | no | — | | | `conversation_id` | string | no | a new conversation (send) | | | `user_context` | object | no | the request's network location | Where and when the end user is. Optional: the agent uses it for local time and as the default schedule time zone; when absent the gateway derives a time zone and approximate place from the request's network location. Values that do not validate (an unknown zone, a malformed locale) are dropped. | # plungeai_build_workflow Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_build_workflow Generate a new workflow from a goal, or refine an existing one (workflow_id + instruction), via the platform builder. Saves it so it appears everywhere (live sync). | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `goal` | string | no | — | | | `workflow_id` | string | no | — | | | `instruction` | string | no | — | | # plungeai_memory Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_memory Long-term memory scoped to you: action recall|remember|search_runs|get_run|forget. remember writes the durable store that recall reads (target user|memory, operation add|replace|remove|read) — use it when the user asks to remember something. forget removes one entry by entry_id, or every entry containing content, from both stores. Past-run history is separate: search_runs/get_run read the run journal, which recall deliberately does not include. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `action` | enum<string> | yes | — | | | `query` | string | no | — | | | `run_id` | string | no | — | | | `entry_id` | string | no | — | | | `target` | enum<string> | no | memory | | | `operation` | enum<string> | no | add | | | `content` | string | no | — | | | `old_text` | string | no | — | | | `limit` | integer | no | 10 | | # plungeai_templates Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_templates Workflow templates: action list|get|use. "use" creates a workflow from the template (optional name + folder). | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `action` | enum<string> | yes | — | | | `template_id` | string | no | — | | | `category` | string | no | — | | | `name` | string | no | <template name> (copy); a bot template keeps its own name | | | `folder` | string | no | — | | | `limit` | integer | no | 50 | | # plungeai_schedule Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_schedule Scheduled (cron) jobs: action stats|list|get|create|update|pause|resume|delete|run_now|runs|wake. create needs name, job_type (agent|query|workflow|heartbeat), target, schedule; create also takes mission_ref (schedule a pre-built agent card), ends_at, and schedule "@once" with run_at (an ISO-8601 time with a UTC offset) for a one-time run. wake (job_id, note) fires the job's saved workflow once. Triggers of a job (job_id): webhook_mint|webhook_list|webhook_delete, email_mint|email_list|email_delete, hooks_mint|hooks_list|hooks_delete (trigger_id deletes one; a secret is shown once, at mint); webhook_rotate|email_rotate (trigger_id) mint a successor with the same settings and revoke the old one. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `action` | enum<string> | yes | — | | | `job_id` | string | no | — | | | `name` | string | no | — | | | `description` | string | no | — | | | `job_type` | enum<string> | no | — | | | `target` | string | no | — | | | `schedule` | string | no | — | | | `timezone` | string | no | UTC | | | `run_at` | string | no | — | | | `ends_at` | string | no | — | | | `parameters` | object | no | — | Job extras. deliver: DeliverTarget[] — objects {channel: "inapp"\|"email"\|"slack"\|"telegram"\|"whatsapp"\|"discord"\|"teams", chat_id?, to?}; bare {channel:"slack"} DMs the owner, bare {channel:"email"} mails the owner's registered address, other emails must be confirmed recipients. prompt: per-run goal. | | `limit` | integer | no | 50 for list, 20 for runs | | | `mission_ref` | string | no | — | | | `check_agent` | string | no | — | | | `check_workflow` | string | no | — | | | `condition_prompt` | string | no | — | | | `trigger_workflow` | string | no | — | | | `notify_channel` | enum<string> | no | — | | | `notify_chat_id` | string | no | — | | | `note` | string | no | — | | | `trigger_id` | string | no | — | | | `prompt_template` | string | no | — | | | `scheme` | string | no | generic | | | `filters` | object | no | — | | | `allowed_from` | string | no | anyone | | | `url` | string | no | — | | | `events` | string[] | no | — | | # plungeai_whoami Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_whoami Show the authenticated identity: user id, tier, key label, rate-limit window. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `format` | enum<string> | no | markdown | markdown (default): rendered for an AI reader · json: the same outcome as a JSON document in `text` plus `structuredContent` | # plungeai_companies Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_companies Your companies (campuses compiled from saved campus + building + office documents), the same as the One API /v1/companies: action list|apply|layout|snapshot|events|ingest|slice; items item_create|item_get|item_pause|item_resume|item_retry|item_cancel|item_steer (item_id, text; item_get returns the item in full with its runs, approvals and history); the gates at the door approvals|decide (approval_id, decision approve|reject — only after the user decided; approving a tool permission continues the paused run once); mode (supervised|tiered|autonomous), start|stop (the clock), delete (refused with conflict while running — stop it first). Every action but list takes campus. Refusals name not_found, conflict, invalid or unavailable. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `action` | enum<string> | yes | — | | | `campus` | string | no | — | | | `building` | string | no | — | | | `since` | integer | no | 0 | | | `limit` | integer | no | 200 | | | `events` | object[] | no | — | | | `item_id` | string | no | — | | | `title` | string | no | — | | | `body` | string | no | — | | | `kind` | enum<string> | no | — | | | `priority` | integer | no | — | | | `tier` | string | no | — | | | `text` | string | no | — | | | `status` | enum<string> | no | all | | | `approval_id` | string | no | — | | | `decision` | enum<string> | no | — | | | `note` | string | no | — | | | `mode` | enum<string> | no | — | | # plungeai_connections Source: https://www.plungeai.com/docs/mcp-reference/tools/plungeai_connections Your connected accounts, the same as the One API /v1/connections: action list (provider, method, status — never a credential)|add (provider + api_key[, api_secret] for a key-based provider)|remove (provider)|link (provider[, return_url]: a signed single-use Studio link, 15 minutes, to connect an OAuth or key provider — send it to whoever owns the account). on_behalf_of acts on one of your customers instead of you. | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `user_request` | string | no | — | The user's original request, verbatim and in their own words, before you translated it into this call. Always include it — the platform uses it for request context, routing and support diagnostics. | | `action` | enum<string> | yes | — | | | `provider` | string | no | — | | | `on_behalf_of` | string | no | you | | | `api_key` | string | no | — | | | `api_secret` | string | no | — | | | `return_url` | string | no | — | | # PlungeAI Agents Source: https://www.plungeai.com/docs/mcp-reference/resources/agents-list `plungeai://agents/list` · `text/markdown` · `static` The live registry of **active** agents: `kind=agents&status=active&format=md&limit=100`. The Markdown starts `# Registry search`, then `*<n> results*`. Each agent follows under a `## Name (id)` heading with its kind, category and status line, its description and a **Good at** list. On 2026-09-24 the registry listed **92** active agents (live-verified). The listing is large, about 150 KB, so attach it only when you really want the whole catalog. For a normal question, a filtered `plungeai_list_agents` search is far smaller. ```bash # verify # expect: Registry search # expect: status:active curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":6,"method":"resources/read","params":{"uri":"plungeai://agents/list"}}' \ | cut -c1-1200 ``` ## Result shape `resources/read` answers one entry: ```json { "contents": [ { "uri": "plungeai://agents/list", "mimeType": "text/markdown", "text": "…" } ] } ``` ## Errors `resources/read` answers JSON-RPC errors, not outcomes; the SDK prefixes each message with `MCP error <code>:` ([resources/read](/mcp-reference/protocol/resources-read)). - `-32603` `Registry error <status>`: the registry answered with a non-2xx status. - `-32002` `Resource not found: <uri>`: the URI matches no resource or template. # Agent Categories Source: https://www.plungeai.com/docs/mcp-reference/resources/agents-categories `plungeai://agents/categories` · `text/markdown` · `static` Active agents (up to 500) grouped by category into one table, largest first. An agent with no category counts as `general`: ``` # Agent Categories (<n> agents, live registry) | Category | Agents | |:---|---:| | google-workspace | … | … _Read `plungeai://agents/{category}` for the agents in one category._ ``` This is the cheap way to answer "what kinds of agents are there". On 2026-09-24 the registry held 92 active agents in 14 categories (live-verified): `google-workspace`, `tools`, `search`, `microsoft-365`, `ai`, `blockchain`, `communication`, `ecommerce-agents`, `payment`, `entertainment`, `media`, `financials`, `shopping`, `coding-agents`. Note the finance category is `financials`, not `finance`. ```bash # verify # expect: Agent Categories # expect: | Category | Agents | curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":7,"method":"resources/read","params":{"uri":"plungeai://agents/categories"}}' ``` ## Result shape `resources/read` answers one entry: ```json { "contents": [ { "uri": "plungeai://agents/categories", "mimeType": "text/markdown", "text": "…" } ] } ``` ## Errors `resources/read` answers JSON-RPC errors, not outcomes; the SDK prefixes each message with `MCP error <code>:` ([resources/read](/mcp-reference/protocol/resources-read)). - `-32603` `Registry error <status>`: the registry answered with a non-2xx status. - `-32002` `Resource not found: <uri>`: the URI matches no resource or template. # Personas Source: https://www.plungeai.com/docs/mcp-reference/resources/personas-list `plungeai://personas/list` · `text/markdown` · `static` The persona registry: `kind=personas&format=md&limit=100`, as registry Markdown (a `## Name (id)` heading per persona, its kind and category, **Good at**). Unlike the agent resources, it has **no** `status:active` filter, because no persona card carries that tag and the filter would empty the list (`resources.ts`). The list stops at 100: on 2026-09-24 the registry answered `*100 results*`. For a specific persona, search with `plungeai_list_agents` `{"kind":"personas","search":"…"}`. A persona id goes on `plungeai_execute_agent` `persona` or a mission's `persona` ([Agents & missions](https://mcp.plungeai.com/docs#7-agents--missions)). ```bash # verify # expect: Registry search curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":8,"method":"resources/read","params":{"uri":"plungeai://personas/list"}}' \ | cut -c1-800 ``` ## Result shape `resources/read` answers one entry: ```json { "contents": [ { "uri": "plungeai://personas/list", "mimeType": "text/markdown", "text": "…" } ] } ``` ## Errors `resources/read` answers JSON-RPC errors, not outcomes; the SDK prefixes each message with `MCP error <code>:` ([resources/read](/mcp-reference/protocol/resources-read)). - `-32603` `Registry error <status>`: the registry answered with a non-2xx status. - `-32002` `Resource not found: <uri>`: the URI matches no resource or template. # Your Workflows Source: https://www.plungeai.com/docs/mcp-reference/resources/workflows-list `plungeai://workflows/list` · `text/markdown` · `static` **Your workflows**: the 50 most recently updated saved workflows on your account (what users call "my agents"). One bullet each (bold name, the id as code, then the description) under `# Your Workflows (<n>)`. With no saved workflows, the text is `_No workflows yet._`. It carries no folders and no search. For those, use `plungeai_list_workflows`. ```bash # verify # expect: "uri":"plungeai://workflows/list" curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":9,"method":"resources/read","params":{"uri":"plungeai://workflows/list"}}' ``` ## Result shape `resources/read` answers one entry: ```json { "contents": [ { "uri": "plungeai://workflows/list", "mimeType": "text/markdown", "text": "…" } ] } ``` ## Errors `resources/read` answers JSON-RPC errors, not outcomes; the SDK prefixes each message with `MCP error <code>:` ([resources/read](/mcp-reference/protocol/resources-read)). - `-32002` `Resource not found: <uri>`: the URI matches no resource or template. # Workflow Authoring Guide Source: https://www.plungeai.com/docs/mcp-reference/resources/docs-workflow-authoring `plungeai://docs/workflow-authoring` · `text/markdown` · `static` The complete CNL workflow-authoring skill as one Markdown document: how to author, validate and save CNL workflows, with examples. It is a static bundle built into the server and needs no data access. The content is identical to `GET https://mcp.plungeai.com/llms-full.txt`, which needs no key (61,309 bytes on 2026-09-24, live-verified). Attach it when you want your client to write CNL YAML by hand ([Workflows](https://mcp.plungeai.com/docs#8-workflows)). The same skill is in the catalog at `https://skills.plungeai.com`. ```bash # verify # expect: PlungeAI Workflow Authoring curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":10,"method":"resources/read","params":{"uri":"plungeai://docs/workflow-authoring"}}' \ | cut -c1-600 ``` ## Result shape `resources/read` answers one entry: ```json { "contents": [ { "uri": "plungeai://docs/workflow-authoring", "mimeType": "text/markdown", "text": "…" } ] } ``` ## Errors `resources/read` answers JSON-RPC errors, not outcomes; the SDK prefixes each message with `MCP error <code>:` ([resources/read](/mcp-reference/protocol/resources-read)). - `-32002` `Resource not found: <uri>`: the URI matches no resource or template. # Agents by category Source: https://www.plungeai.com/docs/mcp-reference/resources/agents-by-category `plungeai://agents/{category}` · `text/markdown` · `template` The active agents in one category, up to 100, as registry Markdown under `# Registry search — category=<category>`. The category is passed to the registry as-is, so an unknown or misspelled category is **not** an error. It returns an empty listing (`*0 results*`). Take category names from `plungeai://agents/categories`. ```bash # verify # expect: category=financials # expect: sec-agent curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":11,"method":"resources/read","params":{"uri":"plungeai://agents/financials"}}' ``` ## Result shape `resources/read` answers one entry: ```json { "contents": [ { "uri": "plungeai://agents/{category}", "mimeType": "text/markdown", "text": "…" } ] } ``` ## Errors `resources/read` answers JSON-RPC errors, not outcomes; the SDK prefixes each message with `MCP error <code>:` ([resources/read](/mcp-reference/protocol/resources-read)). - `-32603` `Registry error <status>`: the registry answered with a non-2xx status. - `-32002` `Resource not found: <uri>`: the URI matches no resource or template. # Workflow YAML by id Source: https://www.plungeai.com/docs/mcp-reference/resources/workflow-yaml `plungeai://workflows/{id}` · `text/yaml` · `template` The saved CNL YAML of one of **your** workflows, as `text/yaml`. The id is the workflow id from `plungeai_list_workflows` or `plungeai://workflows/list`. The ownership check comes first: an id that does not exist and an id owned by someone else get the same `-32002` answer, so you cannot probe for other accounts' ids. A workflow whose YAML body is missing from storage returns the text `No YAML content found`. ```bash # needs one of your workflow ids — not auto-verified curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":12,"method":"resources/read","params":{"uri":"plungeai://workflows/<workflow id>"}}' ``` ## Result shape `resources/read` answers one entry: ```json { "contents": [ { "uri": "plungeai://workflows/{id}", "mimeType": "text/yaml", "text": "…" } ] } ``` ## Errors `resources/read` answers JSON-RPC errors, not outcomes; the SDK prefixes each message with `MCP error <code>:` ([resources/read](/mcp-reference/protocol/resources-read)). - `-32002` `Workflow not found: <id>`: the workflow is unknown or not yours. - `-32002` `Resource not found: <uri>`: the URI matches no resource or template. # Execution result by id Source: https://www.plungeai.com/docs/mcp-reference/resources/execution-result `plungeai://executions/{id}` · `text/markdown` · `template` The stored result of one of **your** runs, by **execution id** (not the workflow id, [Troubleshooting](https://mcp.plungeai.com/docs#15-troubleshooting--gotchas)). It reads the same stores as `plungeai_get_result`: the stored result, then the final task's output. The document looks like this: ``` # Execution <execution id> - **Workflow:** `<workflow id>` ## Result <the run's final output> ``` When there is no output, the Result section says why: | Run state | `## Result` text | |---|---| | failed | `_This run failed: <error message>._` (or `no error was recorded`) | | no output yet | `_No result stored yet (may still be running)._` | This resource gives the final result only. For one task's output, the full conversation thread, the "Steps in this run" index or a paused run's ⏸ block, use `plungeai_get_result` ([Runs, results & follow-ups](https://mcp.plungeai.com/docs#9-runs-results--follow-ups)). An unknown id, or one that is not yours, gets `-32002 Execution not found: <id>`. ```bash # needs one of your execution ids — not auto-verified curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":13,"method":"resources/read","params":{"uri":"plungeai://executions/<execution id>"}}' ``` ## Result shape `resources/read` answers one entry: ```json { "contents": [ { "uri": "plungeai://executions/{id}", "mimeType": "text/markdown", "text": "…" } ] } ``` ## Errors `resources/read` answers JSON-RPC errors, not outcomes; the SDK prefixes each message with `MCP error <code>:` ([resources/read](/mcp-reference/protocol/resources-read)). - `-32002` `Execution not found: <id>`: the execution is unknown or not yours. - `-32002` `Resource not found: <uri>`: the URI matches no resource or template. # plungeai Source: https://www.plungeai.com/docs/mcp-reference/prompts/plungeai ## Arguments | Argument | Required | Description | |---|---|---| | `request` | no | What you want to accomplish (optional) | There is exactly one prompt. It activates the whole platform in one step. Earlier recipe prompts (research pipeline, market research, …) were removed on purpose: they showed up as separate slash commands and locked the client into one behaviour, when the tools already cover everything (`prompts.ts`). | Field | Value | |---|---| | `name` | `plungeai` | | `title` | `PlungeAI` | | `description` | `PlungeAI MCP` | | `arguments` | one: `request` (string, **optional**), "What you want to accomplish (optional)" | **Using it in a client.** Clients list MCP prompts in their slash-command or prompt menu. In Claude Code the prompt appears as `/plungeai:plungeai (MCP)`: type `/plungeai` to filter the menu and pick it, or run `/mcp__plungeai__plungeai` directly (the middle part is the server name you chose in `claude mcp add`). Claude Code splits any text after the command on whitespace, one word per argument, so only the first word would reach `request`. Run the prompt bare, then type your request as the next message. Other clients may offer a form field for `request` instead. - run bare (no `request`): the model is told to ask you what you want to accomplish; - with `request` set (for example `research Tesla's Q2`, from a client that offers the field): the model is told to handle that request now. The prompt is a convenience, not a requirement. You can always just talk to your client normally. `prompts/list`: ```bash # verify # expect: "name":"plungeai" # expect: "request" curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"prompts/list"}' ``` `prompts/get` returns `description: "PlungeAI MCP"` and one `user` message (type `text`). It runs nothing, so it is free: ```bash # verify # expect: Now handle this request: research Tesla's Q2 # expect: plungeai_list_workflows curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"prompts/get","params":{"name":"plungeai","arguments":{"request":"research Tesla'"'"'s Q2"}}}' ``` What the message tells the model: - It now has the full platform through the `plungeai_*` tools: about 90 active agents, CNL workflows, missions, long-term memory and execution history. - **Discover.** "Show me my agents/workflows" means saved workflows (`plungeai_list_workflows`). The registry (`plungeai_list_agents` with a natural-language `search`) is for building blocks. Fetch a full card with `agent_id` before first use. - **Execute.** `plungeai_execute_agent` (one agent, optional `session_id`), `plungeai_execute_workflow` (saved `workflow_id` or ad-hoc CNL YAML), `plungeai_run_mission` (bounded autonomous loop with memory). - **Results.** `plungeai_get_result`, `plungeai_get_workflow_status` (poll async runs), `plungeai_executions`, and `plungeai_followup` / `plungeai_continue` for finished or paused runs. - **Approvals.** Relay any "⏸ AWAITING USER APPROVAL" or "⏸ AWAITING USER" block verbatim, never approve on its own, then call `plungeai_continue` with `approve: true` or the user's words. - **Build.** `plungeai_build_workflow` generates and saves a workflow. `plungeai_workflow` is direct CRUD. `plungeai_memory` is long-term memory. - Always pass `user_request`, the user's words verbatim. - The last line is either `Now handle this request: <request>` (the argument is trimmed) or `Ask the user what they want to accomplish.` when `request` is empty or absent. An unknown prompt name is **rejected** with `-32602`. The SDK prefixes the message with `MCP error -32602:`: ```bash # verify # expect: "code":-32602 # expect: Prompt not found: research curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"research"}}' ``` ## Returned message `prompts/get` returns one `user` message with this text (`orchestration/mcp-gateway/prompts.ts`): ```text "Returned message" [expandable] You now have the full PlungeAI platform available through the plungeai_* tools: ~90 active agents (search, financial, documents, social, payments, automation, …), CNL YAML workflows, autonomous missions, long-term memory, and execution history. How to work with it: - **Discover**: "show me my agents/workflows" means the user's SAVED WORKFLOWS → plungeai_list_workflows. The registry (plungeai_list_agents, search:"<capability in natural language>" — hybrid semantic search, trust the ranking; fetch a full card with agent_id before first use) is the capability catalog of building-block agents — use it when the user says registry/discovery, or when composing workflows. - **Execute**: plungeai_execute_agent (one agent, optional session_id for conversations), plungeai_execute_workflow (saved workflow_id or ad-hoc CNL YAML — sequential, parallel, dynamic, batch), plungeai_run_mission (bounded autonomous loop with memory). - **Results**: plungeai_get_result and plungeai_get_workflow_status (poll async runs); plungeai_executions for history; plungeai_followup / plungeai_continue to keep talking to a finished or paused run. - **Approvals (HITL)**: if a result or status ends with "⏸ AWAITING USER APPROVAL" or "⏸ AWAITING USER", relay it to the user verbatim and ask them to decide (e.g. "Approve? yes/no") — never approve on your own — then plungeai_continue with the execution_id and approve: true or message: "<their words>". - **Build**: plungeai_build_workflow generates and saves a workflow from a goal; plungeai_workflow is direct CRUD. plungeai_memory for long-term memory. - Always pass user_request (the user's words, verbatim) on every call. Ask the user what they want to accomplish. ``` With `request` set, the last line reads `Now handle this request: <request>` instead. # initialize Source: https://www.plungeai.com/docs/mcp-reference/protocol/initialize Opens a conversation: the server answers with the negotiated protocol version, its identity, its capabilities and the `instructions` text your client adds to the model's context. Support: **supported**. Each request stands alone, so `tools/list` and `tools/call` also work without a prior `initialize`. ## Params | Param | Type | Required | Description | |---|---|---|---| | `protocolVersion` | string | yes | The revision your client speaks. `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05` and `2024-10-07` are answered with that version; anything else gets `2025-11-25` | | `capabilities` | object | yes | Your client capabilities; `{}` is fine | | `clientInfo` | object | yes | `{ "name", "version" }` of your client | ## Result | Field | Value | |---|---| | `protocolVersion` | The negotiated version | | `serverInfo` | `{ "name": "plungeai.com", "version": "2.7.0" }`; the version moves with deploys | | `capabilities` | `logging: {}`, `resources: {}`, `prompts: {}`, `tools: { listChanged: true }`; the stateless server never sends `list_changed` | | `instructions` | The server-level usage guide for the model | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' ``` ```json Response {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"logging":{},"resources":{},"prompts":{},"tools":{"listChanged":true}},"serverInfo":{"name":"plungeai.com","version":"2.7.0"},"instructions":"…"}} ``` The response above is abridged: the long `instructions` string is shortened to `…`. ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 200 | `-32602` | `Invalid params: <fields>` for malformed params, for example a numeric `protocolVersion` and no `clientInfo` | | 400 | `-32600` | A batch that holds `initialize` together with other messages | | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). ## notifications/initialized After `initialize` your client sends `notifications/initialized`. It has no `id`, so the server answers `202 Accepted` with an empty body. ```bash curl -s -o /dev/null -w '%{http_code}\n' https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' ``` # tools/list Source: https://www.plungeai.com/docs/mcp-reference/protocol/tools-list Returns the 22 `plungeai_*` tools in catalog order, or only the tools your key is fenced to. Support: **supported**; there is no pagination, so no `nextCursor` comes back and a `cursor` param is ignored. ## Params | Param | Type | Required | Description | |---|---|---|---| | `cursor` | string | no | Ignored: the full list comes back every time | ## Result | Field | Value | |---|---| | `tools[]` | One entry per tool: `name`, `description` and `inputSchema` (JSON Schema) | | `tools[].annotations` | `{ "readOnlyHint": true }` on the six read-only tools; the other 16 carry no annotations | | `tools[].outputSchema` | On seven tools: the outcome envelope on six, the status object on `plungeai_get_workflow_status` | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' ``` ```json Response {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"plungeai_whoami","description":"…","inputSchema":{"type":"object","properties":{"user_request":{"type":"string"},"format":{"type":"string","enum":["markdown","json"]}}},"annotations":{"readOnlyHint":true}}]}} ``` The response above is abridged to one of the 22 entries, and its `description` string is shortened to `…`. The exact schemas are in [/mcp-tools.json](/mcp-tools.json) and on each [tool page](/mcp-reference/tools/plungeai_whoami). ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). # tools/call Source: https://www.plungeai.com/docs/mcp-reference/protocol/tools-call Runs one tool. Support: **supported**. The answer is a server-sent-event stream: each frame is `event: message` plus one `data:` line, progress notifications come first, and the last frame is the JSON-RPC response. ## Params | Param | Type | Required | Description | |---|---|---|---| | `name` | string | yes | The tool name, for example `plungeai_whoami` | | `arguments` | object | yes | The tool's arguments; always include `user_request`, the user's own words | | `_meta.progressToken` | string or number | no | When set, progress arrives as `notifications/progress`; without it, as `notifications/message` at level `info` | ## Result | Field | Value | |---|---| | `content[]` | `[{ "type": "text", "text": "…" }]`: Markdown by default, JSON text with `format: "json"` | | `structuredContent` | The same answer as an object on tools with an output schema, or with `format: "json"` | | `isError` | `true` only for an `error` outcome | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I"}}}' ``` ```text Response event: message data: {"result":{"content":[{"type":"text","text":"# Who am I\n…"}]},"jsonrpc":"2.0","id":3} ``` The response above is abridged: the tool's Markdown text is shortened to `…`. Four tools accept `mode: "async"` and answer at once with an execution id; poll `plungeai_get_workflow_status`, then read the output with `plungeai_get_result`. A pre-screen answer (unknown or fenced tool, bad arguments) and every edge error come back as plain JSON, so check `Content-Type` before parsing. ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 200 | `-32602` | `Tool not found: <name>`, or `Tool not permitted for this key: <name>` for a fenced key | | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | | 429 | `-32000` | Rate limit reached (counted on `tools/call` only); `Retry-After` and `error.data.retryAfter` give the seconds | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). # resources/list Source: https://www.plungeai.com/docs/mcp-reference/protocol/resources-list Returns the five fixed `plungeai://` resources. Support: **supported**. ## Params None: send `"params": {}` or leave `params` out. ## Result | Field | Value | |---|---| | `resources[].uri` | `plungeai://agents/list`, `plungeai://agents/categories`, `plungeai://personas/list`, `plungeai://workflows/list`, `plungeai://docs/workflow-authoring` | | `resources[].name` | PlungeAI Agents, Agent Categories, Personas, Your Workflows, Workflow Authoring Guide | | `resources[].mimeType` | `text/markdown` for all five | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":4,"method":"resources/list"}' ``` ```json Response {"jsonrpc":"2.0","id":4,"result":{"resources":[{"uri":"plungeai://agents/list","name":"PlungeAI Agents","description":"Live agent registry (markdown)","mimeType":"text/markdown"}]}} ``` The response above is abridged to one of the five entries; every resource has its own page under [Resources](/mcp-reference/resources/agents-list). ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). # resources/templates/list Source: https://www.plungeai.com/docs/mcp-reference/protocol/resources-templates-list Returns the three URI templates for resources addressed by a parameter. Support: **supported**. ## Params None: send `"params": {}` or leave `params` out. ## Result | Field | Value | |---|---| | `resourceTemplates[].uriTemplate` | `plungeai://agents/{category}`, `plungeai://workflows/{id}`, `plungeai://executions/{id}` | | `resourceTemplates[].name` | Agents by category, Workflow YAML by id, Execution result by id | | `resourceTemplates[].mimeType` | `text/markdown`, `text/yaml`, `text/markdown` | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":5,"method":"resources/templates/list"}' ``` ```json Response {"jsonrpc":"2.0","id":5,"result":{"resourceTemplates":[{"uriTemplate":"plungeai://agents/{category}","name":"Agents by category","mimeType":"text/markdown"},{"uriTemplate":"plungeai://workflows/{id}","name":"Workflow YAML by id","mimeType":"text/yaml"},{"uriTemplate":"plungeai://executions/{id}","name":"Execution result by id","mimeType":"text/markdown"}]}} ``` ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). # resources/read Source: https://www.plungeai.com/docs/mcp-reference/protocol/resources-read Reads one resource or template instance. Support: **supported**. ## Params | Param | Type | Required | Description | |---|---|---|---| | `uri` | string | yes | A static URI or a filled-in template, for example `plungeai://agents/categories` | ## Result | Field | Value | |---|---| | `contents[]` | One entry: `{ "uri", "mimeType", "text" }` | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":6,"method":"resources/read","params":{"uri":"plungeai://agents/categories"}}' ``` ```json Response {"jsonrpc":"2.0","id":6,"result":{"contents":[{"uri":"plungeai://agents/categories","mimeType":"text/markdown","text":"…"}]}} ``` The response above is abridged: the resource's Markdown `text` is shortened to `…`. ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 200 | `-32002` | `MCP error -32002: Resource not found: <uri>`; also `Workflow not found` and `Execution not found` for the two id templates | | 200 | `-32603` | `Registry error <status>` when the registry behind an agent, category or persona resource fails, or a missing `uri` | | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). # prompts/list Source: https://www.plungeai.com/docs/mcp-reference/protocol/prompts-list Returns the one prompt, `plungeai`. Support: **supported**. In Claude Code it appears as `/plungeai:plungeai (MCP)`. ## Params None: send `"params": {}` or leave `params` out. ## Result | Field | Value | |---|---| | `prompts[].name` | `plungeai` | | `prompts[].title` | `PlungeAI` | | `prompts[].description` | `PlungeAI MCP` | | `prompts[].arguments` | One: `request` (optional), "What you want to accomplish (optional)" | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":7,"method":"prompts/list"}' ``` ```json Response {"jsonrpc":"2.0","id":7,"result":{"prompts":[{"name":"plungeai","title":"PlungeAI","description":"PlungeAI MCP","arguments":[{"name":"request","description":"What you want to accomplish (optional)","required":false}]}]}} ``` ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). # prompts/get Source: https://www.plungeai.com/docs/mcp-reference/protocol/prompts-get Returns the prompt's message. Support: **supported**. It runs nothing, so it is free. ## Params | Param | Type | Required | Description | |---|---|---|---| | `name` | string | yes | `plungeai` | | `arguments.request` | string | no | What the user wants; without it the message tells the model to ask | ## Result | Field | Value | |---|---| | `description` | `PlungeAI MCP` | | `messages[]` | One `user` message of type `text`, ending with `Now handle this request: <request>` or `Ask the user what they want to accomplish.` | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":8,"method":"prompts/get","params":{"name":"plungeai","arguments":{"request":"summarise my last run"}}}' ``` ```json Response {"jsonrpc":"2.0","id":8,"result":{"description":"PlungeAI MCP","messages":[{"role":"user","content":{"type":"text","text":"…Now handle this request: summarise my last run"}}]}} ``` The response above is abridged: the start of the prompt text is shortened to `…`; only its last sentence is shown. ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 200 | `-32602` | `MCP error -32602: Prompt not found: <name>` | | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). # ping Source: https://www.plungeai.com/docs/mcp-reference/protocol/ping A liveness check. Support: **supported**. ## Params None: send `"params": {}` or leave `params` out. ## Result | Field | Value | |---|---| | `result` | `{}` | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":9,"method":"ping"}' ``` ```json Response {"jsonrpc":"2.0","id":9,"result":{}} ``` ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). # logging/setLevel Source: https://www.plungeai.com/docs/mcp-reference/protocol/logging-set-level Support: **partial**. The server answers `{}`, but each request gets a fresh server instance, so the level is not applied to later calls. Progress fallback lines are always sent at level `info`. ## Params | Param | Type | Required | Description | |---|---|---|---| | `level` | string | yes | An MCP logging level, for example `info` | ## Result | Field | Value | |---|---| | `result` | `{}` | ## Example ```bash Request curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":10,"method":"logging/setLevel","params":{"level":"info"}}' ``` ```json Response {"jsonrpc":"2.0","id":10,"result":{}} ``` ## Errors Protocol errors mean the request never reached a tool; the body is `{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}`. | HTTP | Code | When | |---|---|---| | 401 | `-32001` | No key, or a wrong, revoked or expired key, or a bearer that does not start with `ozk_` | The full table is on the [MCP Reference overview](/mcp-reference/overview#errors). # Get balance Source: https://www.plungeai.com/docs/account-api/balance/get-balance <Info> Planned: not available yet. Tracked as TI-02. </Info> Dashboard view, not an API. ## What this will do A key-authenticated route will return the wallet balance. Today **Dashboard → One API → Billing** shows the wallet balance, lifetime top-ups and spend, the auto-recharge setting, the account status and trial allowance, and the saved card (brand and last four digits). ## Use this today <Card title="Open the Billing tab" icon="credit-card" href="https://dashboard.plungeai.com/one-api?tab=billing"> Top up the wallet and see spend in Dashboard → One API → Billing. </Card> <!-- placeholder-source: content/_placeholders.json --> # Add to balance Source: https://www.plungeai.com/docs/account-api/balance/add-to-balance <Info> Planned: not available yet. Tracked as TI-02. </Info> Dashboard view, not an API. ## What this will do A key-authenticated route will add funds to the wallet. Today a top-up in **Dashboard → One API → Billing** opens a Stripe Checkout page; amounts run from 5 to 10,000 USD. ## Use this today <Card title="Open the Billing tab" icon="credit-card" href="https://dashboard.plungeai.com/one-api?tab=billing"> Top up the wallet and see spend in Dashboard → One API → Billing. </Card> <!-- placeholder-source: content/_placeholders.json --> # Create key Source: https://www.plungeai.com/docs/account-api/keys/create-key <Info> Planned: not available yet. Tracked as TI-01. </Info> Dashboard view, not an API. ## What this will do In **Dashboard → One API → Keys** you name a key (1 to 64 characters), pick an expiry (never, 7, 30, 90, 180 or 365 days) and copy the secret, which is shown once. At most 25 keys can be active. A new key takes the account's tier. Legacy model keys (`sk-ocean-`) and connector keys (`sk-conn-`) are no longer created; the Keys tab lists them read-only with revoke. Authentication says which routes each key opens. ## Use this today <CardGroup cols={2}> <Card title="Open the Keys tab" icon="key" href="https://dashboard.plungeai.com/one-api?tab=keys"> Create and revoke keys in the Dashboard. </Card> <Card title="Authentication" icon="shield" href="/getting-started/authentication"> The one key and which routes accept it. </Card> </CardGroup> <!-- placeholder-source: content/_placeholders.json --> # Delete key Source: https://www.plungeai.com/docs/account-api/keys/delete-key <Info> Planned: not available yet. Tracked as TI-01. </Info> Dashboard view, not an API. ## What this will do A key-authenticated route will revoke a key. Today you revoke a key in **Dashboard → One API → Keys**; revocation takes effect within about a minute. ## Use this today <Card title="Open the Keys tab" icon="key" href="https://dashboard.plungeai.com/one-api?tab=keys"> Create and revoke keys in the Dashboard. </Card> <!-- placeholder-source: content/_placeholders.json --> # Get usage Source: https://www.plungeai.com/docs/account-api/usage/get-usage <Info> Planned: not available yet. Tracked as TI-03. </Info> Dashboard view, not an API. ## What this will do A key-authenticated route will return usage and cost. Today **Dashboard → One API → Requests** and **Billing** show runs, tokens and cost, grouped by user, workflow, model or channel, over 7, 30 or 90 days, for you and your connected users. Per-run cost is in traces. ## Use this today <CardGroup cols={2}> <Card title="Open the Billing tab" icon="credit-card" href="https://dashboard.plungeai.com/one-api?tab=billing"> Top up the wallet and see spend in Dashboard → One API → Billing. </Card> <Card title="History & Cost" icon="chart" href="/traces/history-and-cost"> Read per-run cost from traces. </Card> </CardGroup> <!-- placeholder-source: content/_placeholders.json --> # Who am I Source: https://www.plungeai.com/docs/account-api/identity/whoami <Note> Available now over MCP; a REST route is tracked as TI-79. </Note> Read the identity behind your key: the user, the auth type and tier, the key label and id, and the current rate-limit window. It is the `plungeai_whoami` tool of the MCP server, so any MCP client can call it, and it is read-only. <!-- Handler: orchestration/mcp-gateway/extras.ts:692-737 (WhoamiSchema and plungeaiWhoami: the Markdown text at :725, the JSON fields at :728-735; tier, key_label and key_id are null when absent). --> ## Endpoint ```text POST https://mcp.plungeai.com/v1 · tools/call · name: plungeai_whoami ``` ## Authorizations `Authorization: Bearer $PLUNGE_API_KEY` or `X-API-Key: $PLUNGE_API_KEY`, an `ozk_` key from **Dashboard → One API → Keys**. The call counts toward your rate limit, so the window it reports includes it. ## Arguments <ParamField body="user_request" type="string"> The user's words, up to 4,000 characters. Optional. </ParamField> <ParamField body="format" type="string" default="markdown"> `markdown` or `json`. `json` returns the fields below as a JSON document and as `structuredContent`. </ParamField> ## Output With `format: "markdown"` (the default) the text starts with `# Who am I` and lists User, Auth (type and tier), Key (label and key id), Rate window (`<n>/min used · <n>/day used`, left out when the counter cannot be read) and Server. With `format: "json"`: <ResponseField name="user" type="string"> Your user id. </ResponseField> <ResponseField name="auth_type" type="string"> How the request authenticated, `api-key` for an `ozk_` key. </ResponseField> <ResponseField name="tier" type="string | null"> The key's tier: `free`, `pro` or `enterprise`, or `null` when the key carries none. </ResponseField> <ResponseField name="key_label" type="string | null"> The name you gave the key in the Dashboard, or `null` when it has none. </ResponseField> <ResponseField name="key_id" type="string | null"> The key's id, or `null` when it is not known. </ResponseField> <ResponseField name="rate_window" type="object | null"> `{ "minute_used", "day_used" }` for the current windows, or `null` when the counter cannot be read. </ResponseField> <ResponseField name="server" type="string"> The server name and host, `plungeai.com (mcp.plungeai.com)`. </ResponseField> ## Example ```python Python import json, os, requests r = requests.post( "https://mcp.plungeai.com/v1", headers={ "Authorization": f"Bearer {os.environ['PLUNGE_API_KEY']}", "Content-Type": "application/json", "Accept": "application/json, text/event-stream", }, json={"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "plungeai_whoami", "arguments": {"user_request": "who am I?", "format": "json"}}}, ) frames = [line[len("data: "):] for line in r.text.splitlines() if line.startswith("data: ")] print(json.loads(frames[-1])["result"]["structuredContent"]) ``` ```typescript TypeScript import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' const transport = new StreamableHTTPClientTransport(new URL('https://mcp.plungeai.com/v1'), { requestInit: { headers: { Authorization: `Bearer ${process.env.PLUNGE_API_KEY}` } }, }) const client = new Client({ name: 'whoami-example', version: '1.0.0' }) await client.connect(transport) const result = await client.callTool({ name: 'plungeai_whoami', arguments: { user_request: 'who am I?' } }) console.log(result.content) ``` ```bash cURL curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer $PLUNGE_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I?"}}}' ``` Result shape, Markdown form (the field layout of the tool reference; every value is a placeholder): ```markdown # Who am I - **User:** `00000000-0000-4000-8000-000000000005` - **Auth:** api-key · tier **free** - **Key:** my-laptop (`00000000-0000-4000-8000-000000000008`) - **Rate window:** 1/min used · 12/day used - **Server:** plungeai.com (mcp.plungeai.com) ``` Result shape, JSON form (`format: "json"`, the `structuredContent`; every value is a placeholder): ```json { "user": "00000000-0000-4000-8000-000000000005", "auth_type": "api-key", "tier": "free", "key_label": "my-laptop", "key_id": "00000000-0000-4000-8000-000000000008", "rate_window": { "minute_used": 1, "day_used": 12 }, "server": "plungeai.com (mcp.plungeai.com)" } ``` The same tool is documented field by field on [plungeai_whoami](/mcp-reference/tools/plungeai_whoami). # Agent Skills Source: https://www.plungeai.com/docs/skills ## Install [Download all skills (zip)](https://skills.plungeai.com/plungeai-agent-skills.zip) The public repository is publishing soon — until then, download a zip from any skill below. ## Browse All (33) · [Start here (5)](#start-here) · [Capability (13)](#capability) · [In your tool (12)](#in-your-tool) · [Studio kinds (3)](#studio-kinds) ## Start here <CardGroup cols={2}> <Card title="choose-your-plungeai-door" icon="sparkles"> Picks the right PlungeAI entry point before you write anything — the MCP server (`https://mcp.plungeai.com/v1`) for live agent operation, the One API (`https://api.plungeai.com`) for code you write or… [View skill](/skills/choose-your-plungeai-door) · [Download zip](https://skills.plungeai.com/choose-your-plungeai-door.zip) </Card> <Card title="plungeai-api-setup" icon="sparkles"> Setup for the PlungeAI One API (`https://api.plungeai.com`, v2.3.0) — a self-service ozk_ key, the Bearer/X-API-Key header, your first curl call, the live OpenAPI 3.1 contract at /v1/openapi.json… [View skill](/skills/plungeai-api-setup) · [Download zip](https://skills.plungeai.com/plungeai-api-setup.zip) </Card> <Card title="plungeai-cli-setup" icon="sparkles"> Install and log in to the PlungeAI Ocean CLI (ocean-cli, npm package @plungeai/ocean) and run your first commands. [View skill](/skills/plungeai-cli-setup) · [Download zip](https://skills.plungeai.com/plungeai-cli-setup.zip) </Card> <Card title="plungeai-mcp-setup" icon="sparkles"> Connect and operate the PlungeAI MCP server (`https://mcp.plungeai.com/v1`) — endpoint and Bearer ozk_/X-API-Key auth, a generic MCP client config (native remote/HTTP, or the mcp-remote stdio bridge)… [View skill](/skills/plungeai-mcp-setup) · [Download zip](https://skills.plungeai.com/plungeai-mcp-setup.zip) </Card> <Card title="plungeai-sdk-setup" icon="sparkles"> Install and use the official PlungeAI One API SDKs — TypeScript `@plungeai/one-api` (npm), Python `plungeai` (PyPI), Go `github.com/PlungeAI/plungeai-go` and Rust `plungeai` (crates.io) — for the… [View skill](/skills/plungeai-sdk-setup) · [Download zip](https://skills.plungeai.com/plungeai-sdk-setup.zip) </Card> </CardGroup> ## Capability <CardGroup cols={2}> <Card title="ocean-cli" icon="sparkles"> Drive PlungeAI Ocean from a terminal with the ocean-cli command (package @plungeai/ocean): web search, extract and deep research, registry agents and tool agents, saved workflows and bots… [View skill](/skills/ocean-cli) · [Download zip](https://skills.plungeai.com/ocean-cli.zip) </Card> <Card title="plungeai-agents" icon="sparkles"> Run a PlungeAI registry agent: recognize the two kinds (prompt-driven vs structured tool-agents), list them (`GET /v1/agents`), execute a prompt-driven one sync or async (`plungeai_execute_agent`… [View skill](/skills/plungeai-agents) · [Download zip](https://skills.plungeai.com/plungeai-agents.zip) </Card> <Card title="plungeai-campaigns" icon="sparkles"> Run a list to completion on PlungeAI: the campaign ledger (claim/complete/fail/retry), the campaign-config (list source, cycle, cadence, retries, delivery), and how a campaign keeps going via cron… [View skill](/skills/plungeai-campaigns) · [Download zip](https://skills.plungeai.com/plungeai-campaigns.zip) </Card> <Card title="plungeai-discovery" icon="sparkles"> Find the right thing on PlungeAI's live registry before building anything: agents, structured tools, models, skills, personas, connectors, and workflow templates, via hybrid semantic + keyword… [View skill](/skills/plungeai-discovery) · [Download zip](https://skills.plungeai.com/plungeai-discovery.zip) </Card> <Card title="plungeai-memory" icon="sparkles"> Read and write PlungeAI's per-user long-term memory over MCP (plungeai_memory: recall/remember/search_runs/get_run), the CLI (`ocean-cli memory`) and the One API (`/v1/memory/recall`, `/remember`… [View skill](/skills/plungeai-memory) · [Download zip](https://skills.plungeai.com/plungeai-memory.zip) </Card> <Card title="plungeai-missions" icon="sparkles"> Run bounded autonomous PlungeAI agent missions (type: harness) — a goal, a tool fence, an iteration cap, and self-checked success criteria, via plungeai_run_mission, `ocean-cli missions run`, or a… [View skill](/skills/plungeai-missions) · [Download zip](https://skills.plungeai.com/plungeai-missions.zip) </Card> <Card title="plungeai-models" icon="sparkles"> Model routing on PlungeAI: how agents/workflows/missions resolve a model through the platform's provider factory (`model`/`provider` fields on a task, mission or `POST /v1/agents/{id}/execute`), and… [View skill](/skills/plungeai-models) · [Download zip](https://skills.plungeai.com/plungeai-models.zip) </Card> <Card title="plungeai-platform" icon="sparkles"> THE PlungeAI (Ocean) platform capability map: a growing live registry of agents and structured tools, CNL multi-agent workflows, bounded harness missions, injectable skills/plugins/experts/personas… [View skill](/skills/plungeai-platform) · [Download zip](https://skills.plungeai.com/plungeai-platform.zip) </Card> <Card title="plungeai-results-traces" icon="sparkles"> Debug and observe PlungeAI runs: live SSE events, plungeai_get_workflow_status / plungeai_executions history, persisted GET /v1/traces/{id} spans and gateway request cost, the HITL conversation loop… [View skill](/skills/plungeai-results-traces) · [Download zip](https://skills.plungeai.com/plungeai-results-traces.zip) </Card> <Card title="plungeai-scheduling" icon="sparkles"> Cron-schedule PlungeAI workflows, agent queries, pre-built agent cards, and condition-watching heartbeats via plungeai_schedule, with full run history and retries. [View skill](/skills/plungeai-scheduling) · [Download zip](https://skills.plungeai.com/plungeai-scheduling.zip) </Card> <Card title="plungeai-skills-plugins" icon="sparkles"> Declare and understand PlungeAI's capability-injection fields on a type: harness task or plungeai_run_mission — skills, experts, persona, backgrounds, plugins, and MCP servers — including… [View skill](/skills/plungeai-skills-plugins) · [Download zip](https://skills.plungeai.com/plungeai-skills-plugins.zip) </Card> <Card title="plungeai-tools-connectors" icon="sparkles"> Call PlungeAI structured tool-agents with typed parameters against a published invocation contract: fetch the contract (`plungeai_get_tool_contract` / `GET /v1/tools/{id}`), execute with typed… [View skill](/skills/plungeai-tools-connectors) · [Download zip](https://skills.plungeai.com/plungeai-tools-connectors.zip) </Card> <Card title="plungeai-workflows" icon="sparkles"> Build, validate, test, and save PlungeAI (Ocean Studio) workflows in CNL YAML. [View skill](/skills/plungeai-workflows) · [Download zip](https://skills.plungeai.com/plungeai-workflows.zip) </Card> </CardGroup> ## In your tool <CardGroup cols={2}> <Card title="plungeai-in-bolt" icon="sparkles"> Connect Bolt.new to PlungeAI (Ocean Studio) over MCP — the Connectors → Custom MCP server form, the all-tools/all-projects toggle behavior, keeping the shipped app's key server-side, and a… [View skill](/skills/plungeai-in-bolt) · [Download zip](https://skills.plungeai.com/plungeai-in-bolt.zip) </Card> <Card title="plungeai-in-claude-ai" icon="sparkles"> Connect claude.ai web or Claude Desktop to PlungeAI (Ocean Studio) — Desktop live tools via the mcp-remote bridge in claude_desktop_config.json (OAuth isn't available yet, hence the bridge)… [View skill](/skills/plungeai-in-claude-ai) · [Download zip](https://skills.plungeai.com/plungeai-in-claude-ai.zip) </Card> <Card title="plungeai-in-claude-code" icon="sparkles"> Connect the Claude Code CLI to PlungeAI (Ocean Studio) over MCP — 'claude mcp add' one-liner, project-scoped .mcp.json with an env-var key, the skills/plugin install path, and a plungeai_whoami +… [View skill](/skills/plungeai-in-claude-code) · [Download zip](https://skills.plungeai.com/plungeai-in-claude-code.zip) </Card> <Card title="plungeai-in-codex" icon="sparkles"> Connect OpenAI Codex CLI to PlungeAI (Ocean Studio) over MCP — native streamable-HTTP in ~/.codex/config.toml (env-var or static bearer token), the codex mcp add terminal command, the mcp-remote… [View skill](/skills/plungeai-in-codex) · [Download zip](https://skills.plungeai.com/plungeai-in-codex.zip) </Card> <Card title="plungeai-in-cursor" icon="sparkles"> Connect Cursor to PlungeAI (Ocean Studio) over MCP — the one-click install-page deeplink or a ~/.cursor/mcp.json entry, Agent/Plan-mode tool behavior, the ~40-tool cap, and a plungeai_whoami +… [View skill](/skills/plungeai-in-cursor) · [Download zip](https://skills.plungeai.com/plungeai-in-cursor.zip) </Card> <Card title="plungeai-in-gemini-cli" icon="sparkles"> Connect Gemini CLI to PlungeAI (Ocean Studio) over MCP — a ~/.gemini/settings.json entry using httpUrl (not url, which is SSE-only and 405s), the mcp-remote bridge fallback, /mcp reload after edits… [View skill](/skills/plungeai-in-gemini-cli) · [Download zip](https://skills.plungeai.com/plungeai-in-gemini-cli.zip) </Card> <Card title="plungeai-in-lovable" icon="sparkles"> Connect Lovable to PlungeAI (Ocean Studio) — a personal chat connector (any plan) so Lovable's chat can operate PlungeAI via MCP while building, and a workspace-admin app connector so shipped apps… [View skill](/skills/plungeai-in-lovable) · [Download zip](https://skills.plungeai.com/plungeai-in-lovable.zip) </Card> <Card title="plungeai-in-opencode" icon="sparkles"> Connect OpenCode to PlungeAI (Ocean Studio) over MCP — an opencode.json mcp.plungeai entry with type: remote, {env:VAR} variable substitution for a committed project config, and a plungeai_whoami +… [View skill](/skills/plungeai-in-opencode) · [Download zip](https://skills.plungeai.com/plungeai-in-opencode.zip) </Card> <Card title="plungeai-in-replit" icon="sparkles"> Connect Replit Agent to PlungeAI (Ocean Studio) over MCP — the one-click install-page link, the Integrations → MCP Servers form, keeping the Agent's MCP key separate from a deployed app's Replit… [View skill](/skills/plungeai-in-replit) · [Download zip](https://skills.plungeai.com/plungeai-in-replit.zip) </Card> <Card title="plungeai-in-v0" icon="sparkles"> Connect v0 (Vercel) to PlungeAI (Ocean Studio) over MCP — the + menu → MCPs form, why the generated Next.js app must call PlungeAI through a server Route Handler (v0 apps can't call MCP directly)… [View skill](/skills/plungeai-in-v0) · [Download zip](https://skills.plungeai.com/plungeai-in-v0.zip) </Card> <Card title="plungeai-in-vscode" icon="sparkles"> Connect VS Code (GitHub Copilot agent mode) to PlungeAI (Ocean Studio) over MCP — the code --add-mcp CLI command, a committed-safe .vscode/mcp.json with an input-prompted key, the… [View skill](/skills/plungeai-in-vscode) · [Download zip](https://skills.plungeai.com/plungeai-in-vscode.zip) </Card> <Card title="plungeai-in-windsurf" icon="sparkles"> Connect Windsurf (Cascade) to PlungeAI (Ocean Studio) over MCP — a ~/.codeium/windsurf/mcp_config.json entry using serverUrl, the mcp-remote bridge fallback for stdio-only builds, the 100-tool cap… [View skill](/skills/plungeai-in-windsurf) · [Download zip](https://skills.plungeai.com/plungeai-in-windsurf.zip) </Card> </CardGroup> ## Studio kinds <CardGroup cols={2}> <Card title="plungeai-agentic-agent" icon="sparkles"> Design and emit ONE bounded agentic agent for PlungeAI (Ocean Studio) as a single `type: harness` mission — purpose, allowed_tools fence, permissions gates (deny/ask), skills/plugins/MCP/persona… [View skill](/skills/plungeai-agentic-agent) · [Download zip](https://skills.plungeai.com/plungeai-agentic-agent.zip) </Card> <Card title="plungeai-bot-agent" icon="sparkles"> Design and emit ONE PlungeAI (Ocean Studio) bot agent — a scheduled, unattended `type: harness` mission that reports to the user's channels (in-app, email, Slack, Telegram, WhatsApp, Discord, Teams)… [View skill](/skills/plungeai-bot-agent) · [Download zip](https://skills.plungeai.com/plungeai-bot-agent.zip) </Card> <Card title="plungeai-campaign-agent" icon="sparkles"> Design and emit ONE PlungeAI (Ocean Studio) campaign agent — a long-running, list-driven, resumable agent that works an owned ledger of items in short scheduled runs until the current cycle is… [View skill](/skills/plungeai-campaign-agent) · [Download zip](https://skills.plungeai.com/plungeai-campaign-agent.zip) </Card> </CardGroup> # choose-your-plungeai-door Source: https://www.plungeai.com/docs/skills/choose-your-plungeai-door Picks the right PlungeAI entry point before you write anything — the MCP server (`https://mcp.plungeai.com/v1`) for live agent operation, the One API (`https://api.plungeai.com`) for code you write or generate, the Ocean CLI for a terminal or CI workflow, or Ocean Studio for a human building visually — and names the setup or capability skill to load next. Use when the user asks which PlungeAI door, API, or interface to use, says "how do I use PlungeAI", "what's the difference between the MCP server and the One API", asks "does PlungeAI have a CLI/SDK/UI", or is choosing between MCP, REST, CLI, and Studio before writing any code. For the mechanics once a door is picked, use `plungeai-mcp-setup` (MCP), `plungeai-api-setup` (REST/codegen), `plungeai-sdk-setup` (official SDKs), or `plungeai-cli-setup` (terminal); for wiring a specific coding tool's agent to PlungeAI use `plungeai-in-<tool>`; the full capability map lives in `plungeai-platform`. [Download zip](https://skills.plungeai.com/choose-your-plungeai-door.zip) · [View raw SKILL.md](https://skills.plungeai.com/choose-your-plungeai-door/SKILL.md) PlungeAI (internally "Ocean") is a Cloudflare-native agent runtime: every agent, tool, and workflow runs as a deployed edge service. There is no local mode — everything you execute runs on the deployed platform, and everything you read (catalogs, contracts, results) is live. You reach it through one of four doors, all backed by the same identity and the same data (a workflow saved through one door appears in every other door and in Studio). ## The four doors | Door | What it is | Use it when | |---|---|---| | **MCP server** — `https://mcp.plungeai.com/v1` | The `plungeai_*` tool suite over MCP (Streamable HTTP; 22 tools, server 2.7.0) | You are an AI agent operating the platform live in a chat/agent context: discover, execute, save, schedule, remember. The richest surface — structured outcomes, approvals, continuations. | | **One API** — `https://api.plungeai.com` | REST gateway, OpenAPI 3.1 at `GET /v1/openapi.json` (v2.3.0). Official SDKs in TypeScript, Python, Go and Rust (setup: `plungeai-sdk-setup`); setup over plain HTTP: `plungeai-api-setup` | You are writing or GENERATING code that calls PlungeAI: apps, scripts, backends, CI jobs, SDK wrappers — including running and governing **bots** (policy, schedules, triggers, executions) and calling a saved agent by id. | | **Ocean CLI** — `ocean-cli` | Terminal client `ocean-cli` 2.1.0 (package `@plungeai/ocean`, not on npm until the `ocean-v2.1.0` tag is published — run it from the Ocean-Platform repo until then). Setup: `plungeai-cli-setup`, every verb: `ocean-cli`. In Claude Code and OceanCode the plugin adds `/ocean-*` slash commands on top of it | A human (or a script) works from a shell or CI pipeline: auth, doctor, running saved flows, scheduling. | | **Ocean Studio** | Browser app at `https://studio.plungeai.com` | A human builds/edits workflows visually, reviews runs, connects OAuth credentials, configures schedules — anything a door refuses (an OAuth consent, visual editing). | ## The One API has three levels — all stay REST now covers bots, and the same key opens all three levels; none is deprecated in favour of another (`plungeai-api-setup`): 1. **Compatible wire** — `/v1/chat/completions`, `/v1/models` and the OpenRouter-shaped `/api/v1/models`: an OpenAI or OpenRouter client works unchanged (models only). 2. **Standard REST** — one route per resource, JSON or YAML in and out: saved workflows, agents and **bots** (`/v1/workflows`, `/v1/workflows/{id}/permissions`, workspace files, inbox, chat bindings), `/v1/bot-groups`, `/v1/channels`, `/v1/executions` (continue, steer, files), `/v1/schedules` (cron or `@once`) with triggers (`/v1/triggers/webhook`, email, hooks), `/v1/connections`, `/v1/memory/recall`, `/v1/skills`, `/v1/templates`. Start here. 3. **Agent Call door** — `POST /v1/execute`: the body is the program (a bare task, a `workflow:`, or `agent: <saved id>@n` + `input`); always through the engine, so a saved bot keeps its stored policy. The same bot verbs exist as actions on the MCP tools (`plungeai_workflow` permissions, files, bindings and groups; `plungeai_executions` steer and cancel; `plungeai_schedule` wake and triggers) and as `ocean-cli` commands — pick the door by who is calling, not by what it can do. ## Rules of thumb - **Operating live from a chat/agent context → MCP.** One tool call per intent; the platform renders final user-ready markdown — relay it verbatim. Setup: `plungeai-mcp-setup`. Full guide: https://mcp.plungeai.com/docs#0-overview - **Generating code for a user's project → One API.** Point codegen at `GET /v1/openapi.json`; never invent routes. Setup: `plungeai-api-setup`. Building or running a bot: author it with `plungeai-bot-agent`, then drive it over the verbs above. - **An SDK in your own code (TypeScript, Python, Go, Rust) → One API through its official SDK.** Setup: `plungeai-sdk-setup`. - **A human or CI script driving a terminal → Ocean CLI.** Setup: `plungeai-cli-setup`. - **Anything a door refuses (an OAuth consent, visual editing) → a person in a browser:** Ocean Studio, or the hosted connect link `POST /v1/connections/{provider}/link` mints. An API-key connection goes over REST: `POST /v1/connections/{provider}`. - **Connecting a *specific* coding tool's agent** (Cursor, VS Code, Claude Code, Codex, Windsurf, Replit, Gemini CLI, OpenCode, Lovable, Bolt, v0, …) rather than deciding which door in the abstract → jump straight to that tool's `plungeai-in-<tool>` skill; it tells you which door that tool uses and gives the exact config. - Same identity behind every door: one `ozk_` key on MCP, the One API and the CLI (`ocean-cli login`); an optional Studio session adds the CLI's account verbs (keys, balance, usage, teams). ## Get a key first Self-service `ozk_` keys: **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`) → create → copy it once (shown only at creation). Ask the account owner only if you need a shared team key minted under someone else's account. The same key works on MCP, the One API and the Ocean CLI (`ocean-cli login`); the CLI also takes an optional Studio session for its account verbs (keys, balance, usage, teams, secrets). ## Which capability for which job Once you know your door, `plungeai-platform` is the full capability map. Quick router: - **One-shot capability call** ("search the web", "convert this PDF") → `plungeai-agents` (prompt-driven) or `plungeai-tools-connectors` (structured, typed params). - **Multi-step pipeline** ("research A, B, C in parallel, then synthesize") → CNL workflow, author it with `plungeai-workflows`. - **Open-ended goal needing judgment** ("investigate X, use whatever tools you need") → one bounded harness mission, `plungeai-missions`. - **"Every morning / every hour" anything** → `plungeai-scheduling`. - **Long-running lead-gen / outreach style batches** → `plungeai-campaigns`. - **Instruction packs injected into a run** (skills, plugins, experts, personas) → `plungeai-skills-plugins`. - **A standing agent with its own policy, memory and chat** → a bot, authored with `plungeai-bot-agent` (several of them: `/v1/bot-groups`). - **"Remember this" / long-term memory** → `plungeai-memory`. - **Raw LLM inference in your own code** (chat/embeddings, model fallback) → `plungeai-models`. - **"What can PlungeAI do" / finding an id** → `plungeai-discovery`. - **"Why did that run fail / how long / what did it cost"** → `plungeai-results-traces`. ## Discovery first (every door, non-negotiable) Catalogs are live — the agent registry, tool contracts, model list, and workflow inventory change without notice. Never assert what exists from memory: - MCP: `plungeai_list_agents {search: "<capability>"}` - One API: `GET /v1/discovery/search?q=<capability>`, `GET /v1/agents`, the live contract at `GET /v1/openapi.json` - CLI: `ocean-cli discovery search <query>` / `ocean-cli agents get <id>` ## Verify you're in - **MCP:** call `plungeai_whoami` — identity card (user id, tier, key label, rate window). - **One API:** `curl -s https://api.plungeai.com/health` (no auth) proves the router is up; `curl -s https://api.plungeai.com/v1/whoami -H "Authorization: Bearer ozk_YOUR_KEY"` proves the key (`{user_id, via, tier, key_label}`). - **CLI:** `ocean-cli doctor` (exit 0 = ready). ## Related skills - `plungeai-mcp-setup` — connect and operate the MCP server. - `plungeai-api-setup` — auth, first call, OpenAPI, codegen for the One API. - `plungeai-cli-setup` — install, auth, full command reference for `ocean-cli`. - `plungeai-sdk-setup` — the official One API SDKs (TypeScript, Python, Go, Rust): install, key, first call. - `plungeai-in-<tool>` — per-editor connect steps (Cursor, VS Code, Claude Code, Codex, …). - `plungeai-platform` — the full capability map (agents, tools, workflows, models, missions, scheduling, memory, skills/plugins, observability) once you're connected. # plungeai-api-setup Source: https://www.plungeai.com/docs/skills/plungeai-api-setup Setup for the PlungeAI One API (`https://api.plungeai.com`, v2.3.0) — a self-service ozk_ key, the Bearer/X-API-Key header, your first curl call, the live OpenAPI 3.1 contract at /v1/openapi.json, typed-client codegen, the three API levels (compatible wire, standard REST incl. bots, schedules and executions, the Agent Call door `POST /v1/execute`), and shared error/rate-limit basics. Use whenever writing or GENERATING code that calls PlungeAI, or answering "how do I authenticate", "where's the OpenAPI spec", "how do I generate a typed client" or "can I run a bot over REST". Triggers: "PlungeAI API", "api.plungeai.com", "One API", "call plungeai from code", "plungeai openapi", "plungeai curl", "plungeai SDK". Route detail: the capability skills (`plungeai-agents`, `plungeai-workflows`, `plungeai-scheduling`, `plungeai-results-traces`, …). MCP → `plungeai-mcp-setup`; terminal → `plungeai-cli-setup`; unsure which door → `choose-your-plungeai-door`. [Download zip](https://skills.plungeai.com/plungeai-api-setup.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-api-setup/SKILL.md) One base URL for the whole platform: **`https://api.plungeai.com`**. Plain JSON over HTTPS — no SDK required. The models ("money") plane is OpenAI-compatible. Machine-readable contract: `GET /v1/openapi.json` (OpenAPI 3.1). This skill gets you authenticated and making calls; the route-by-route manual for each capability (what params, what the response looks like, error handling per route) lives in that capability's own skill — load it next. ## Prerequisites — get a key Self-service `ozk_` key: **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`) → create → name it, pick an expiry (never, or 7–365 days) → copy it once, it is shown exactly once and stored hashed thereafter. Ask the account owner only for a shared/team key minted under someone else's account. Keys inherit your account's tier (free by default). The same `ozk_` key opens the models plane too (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`, see `plungeai-models`) — one key for every plane. The legacy `sk-ocean-` / `sk-conn-` keys are retired: no new ones can be minted, and existing ones sit read-only under **Dashboard → One API → Keys → Legacy keys** until revoked. ## Discovery first — hard rule The catalog is **live**. Never hardcode agent ids, tool lists, model slugs, or route shapes from memory: - `GET /v1/openapi.json` — the live route contract, no auth required. This is the one authority for what routes exist; regenerate any typed client from it, never patch by hand. - `GET /v1/agents` / `GET /v1/discovery/search?q=<capability>` — what's callable right now (full capability detail: `plungeai-discovery`, `plungeai-agents`). ## Quick start — first call ```bash curl -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "Say exactly: hello ocean", "sync": true}' ``` ```json { "content": "hello ocean", "workflow_id": "00000000-0000-4000-8000-000000000001", "task_id": "t1", "request_id": "00000000-0000-4000-8000-000000000004" } ``` That is the whole integration pattern: bearer key, JSON body, JSON result. Every other plane (tools, workflows, models, MCP pass-through, discovery, traces) works the same shape — the per-plane request/response detail is in that plane's capability skill. ## Three levels — all supported, none deprecated | Level | What | Use it when | |---|---|---| | 1 Compatible wire | `/v1/chat/completions`, `/v1/models`, `/api/v1/*` (models only) | an OpenAI or OpenRouter client should just work | | 2 Standard REST | one route per resource, JSON or YAML in and out: `/v1/workflows` (saved workflows, agents, **bots**, campaigns, versions), `/v1/executions`, `/v1/schedules`, `/v1/triggers`, `/v1/bot-groups`, `/v1/channels`, `/v1/connections`, `/v1/memory`, `/v1/skills`, `/v1/templates` | you manage a resource or call it by id — start here | | 3 Agent Call door | `POST /v1/execute`: the body is the program — a bare task, a `workflow:`, or `agent: <saved id>@3` + `input` | the body is the program; it always runs through the engine, so a saved bot keeps its stored policy | ```bash # a saved bot, pinned to snapshot 3, planned but not run 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}' ``` Bots over REST: set a bot's permission classes, locks and spend caps (`PUT /v1/workflows/{id}/permissions`), read its workspace files and inbox, bind a paired chat (`/bindings`), group bots (`/v1/bot-groups`), steer a running execution (`POST /v1/executions/{id}/steer`), schedule it once (`@once` + `run_at`) or wake it, and mint webhook, email or outbound-hook triggers. Connections (`/v1/connections`, `on_behalf_of` for your customers) are there too. Every route, body field and status of those: [`references/bots-and-door.md`](/skills/plungeai-api-setup/references/bots-and-door); every other operation (discovery, tools, agents, MCP, traces, models): [`references/route-map.md`](/skills/plungeai-api-setup/references/route-map). The saved-bot authoring rules are `plungeai-bot-agent`'s; MCP and CLI spellings are in the verb tables of `plungeai-workflows` and `plungeai-scheduling`. ## Save a workflow, run it, read the result A saved workflow is CNL YAML in the `yaml` field of `POST /v1/workflows` (`kind` is `workflow`, `agent`, `bot` or `campaign`). The smallest valid body, as YAML (a `workflow:` wrapper, `tasks:` list, one `type: task`): ```yaml workflow: name: Hello tasks: - type: task id: t1 agent: one-agent prompt: "Say exactly: {input}" ``` ```bash KEY=ozk_YOUR_KEY; API=https://api.plungeai.com # save → 201 {"workflow":{"id":"<id>",…},"warnings":[]} (a refusal is 422 invalid_workflow with errors[]) curl -s -X POST $API/v1/workflows -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"name":"Hello","kind":"workflow","yaml":"workflow:\n name: Hello\n tasks:\n - type: task\n id: t1\n agent: one-agent\n prompt: \"Say exactly: {input}\"\n"}' # or save a YAML file as the body (kind and name ride the query; workflow.name is the name when ?name is left out) curl -s -X POST "$API/v1/workflows?kind=bot&name=Hello" -H "Authorization: Bearer $KEY" -H 'Content-Type: text/yaml' --data-binary @bot.yaml # run it by id → {"success":true,"workflow_id":"<execution id>","final_task_id":"t1","request_id":"…","content":"…"} curl -s -X POST $API/v1/workflows/<id>/execute -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" -d '{"input":"hello"}' # read it back: the execution id is the `workflow_id` of the run answer (not the saved workflow's id) curl -s $API/v1/executions/<execution id>/output -H "Authorization: Bearer $KEY" ``` Agent ids in `agent:` come from discovery, never memory: `GET /v1/discovery/search?q=<capability>` returns skill and other cards first, so add `&kind=agents` when you want an agent id (`GET /v1/agents` lists them too). Other routes you will want next: `GET /v1/executions/{id}` (poll `status`), `GET /v1/executions/{id}/output`, `GET /v1/executions/{id}/files`, `POST /v1/schedules` — all in [`references/bots-and-door.md`](/skills/plungeai-api-setup/references/bots-and-door). ## Authentication — header and error basics | Key | Planes | How to send | |---|---|---| | `ozk_YOUR_KEY` | Execution planes: `/v1/discovery`, `/v1/tools`, `/v1/agents`, `/v1/workflows`, `/v1/executions`, `/v1/schedules`, `/v1/triggers`, `/v1/bot-groups`, `/v1/channels`, `/v1/connections`, `/v1/memory`, `/v1/skills`, `/v1/templates`, `/v1/execute`, `/v1/mcp`, `/v1/traces` | `Authorization: Bearer ozk_YOUR_KEY` (or `X-API-Key: ozk_YOUR_KEY`) | | `ozk_YOUR_KEY` (the same key) | Models plane: `/v1/chat/completions`, `/v1/embeddings`, `/v1/models` | `Authorization: Bearer ozk_YOUR_KEY` only (no `X-API-Key` here) | - Never hardcode keys in generated code — read the one `ozk_` key from an env var (`PLUNGEAI_API_KEY`) for every plane. - Execution-plane errors use the envelope `{"error":{"code","message",…}}` and always carry `X-Error-Code: <code>` — branch on the header. The body follows the negotiated `format` (`?format=` or a body field, `Accept`: `json` default, `yaml`, `markdown`, `text`); an unknown value is `400 invalid_format`. A 424 adds `error.connect` (`{provider, method, link}`); an error on a call that created an execution carries `X-Execution-Id`. `/v1/discovery/recommend` and `/v1/discovery/cards/:type/:id` normalize the registry's own flat `{"error":"<string>"}` into the envelope (`code` is `not_found` on a 404, `invalid_request` otherwise). The models plane uses the OpenAI error shape (`{"error":{"message","type","code","request_id"}}`); the `/api/v1` drop-in the OpenRouter one. - Every response carries a **server-minted** `x-request-id` header. To correlate your own calls, send **`x-trace-id`** and keep your own copy — it is not echoed back, but it is threaded across every internal hop and queryable at `GET /v1/traces/<your id>` (`plungeai-results-traces`). ## Error semantics — the ones every caller needs | Status | Code | Meaning | What your code does | |---|---|---|---| | 400 | `run_at_required` / `run_at_past` | A `@once` schedule with no `run_at`, or one not in the future | Send an ISO-8601 time WITH a UTC offset, in the future | | 400 | `invalid_json` / `invalid_yaml` / `invalid_body` / `invalid_format` / `missing_workflow` / `ambiguous_body` | An unreadable body, an unknown `format`, none of `workflow` / `agent` / `tasks`, or `agent:` next to `workflow:` | Fix the body; the message names the problem | | 401 | `unauthorized` | Missing/invalid key, or the wrong key prefix for this plane | Fix the key. Never retry in a loop | | 403 | `refused` | **Trust fence.** The agent/tool refused the action outright — a gated/money verb, unattended surface. A policy verdict, not a lifecycle state | **FINAL. Surface verbatim. NEVER retry, rephrase, or route around** | | 403 | `agent_not_active` | A named agent (in a workflow or a single-agent execute) is parked/inactive — a lifecycle state, distinct from `refused` above | Re-discover a fresh id (`GET /v1/discovery/search`); don't retry the same id | | 403 | `lock_admin_required` | A bot's permission locks can be set only by a team admin; nothing in that body was written | Ask an admin; resend without `locks` | | 404 | `unknown_agent` / `unknown_tool` / `agent_not_found` (the `POST /v1/execute` door) / `<thing>_not_found` (`workflow`, `execution`, `schedule`, `trigger`, `group`, `binding`, `version`, `template`, `skill`, `memory`, `connection`, …) | No such id, or another account's (the two answer alike) | Re-discover; don't retry the same id | | 404 | `not_ready` | Async result not landed yet | Poll again with backoff (2s+) | | 409 | `approval_required` | **Trust fence.** A human must approve before the action runs — implemented and produced today, not a placeholder. Three shapes, three answers (below) | Never approve on your own | | 409 | `duplicate_execution_id` | The `x-trace-id` you sent was already used by an earlier run | Send a fresh UUID, or omit it | | 413 | `payload_too_large` | Body over 1 MiB | Shrink the body | | 422 | `invalid_workflow` | A saved workflow, agent or bot fails CNL validation or its kind's lint (`errors` and `warnings` listed); nothing is written | Fix exactly the listed fields and resend | | 422 | `invalid_params` | Outcome `needs_input` — body fails the tool contract (the response echoes `missing` and the contract), or a field on a resource route is missing or malformed | Self-correct from the echoed contract or message, then retry once | | 424 | `connection_required` / `credential_required` | Outcome `needs_connection` / `needs_api_key` — no connected account, or no API key for a connector; `error.connect` names the provider and where to connect it | `POST /v1/connections/{provider}` (a key) or `…/link` (a hosted URL for a customer), or Ocean Studio → Connectors, then retry | | 429 | `rate_limited` | **Execution planes** — per-tier RPM/RPD cap, or failed-auth metering | Honour the `Retry-After` header; back off | | 429 | `rate_limit_exceeded` / `spend_cap_exceeded` / `insufficient_quota` | **Models plane only** — separate codes, separate plane; rate, spend-cap, or quota limit | Back off; caps/quotas are policy, not transients | | 5xx | `engine_error` / `upstream_error` / `storage_error` / `scheduler_error` / `internal_error` (`503 agent_unavailable` / `not_enabled`) | Upstream or router failure, or a feature not switched on yet | One retry with backoff is reasonable | `409 approval_required` — pick the row that matches what you hold: | You have | Approve with | Then | |---|---|---| | A 409 from `POST /v1/tools/{id}/execute` (the dispatch fence; no execution exists to continue) | Studio, by a human | re-issue the identical request | | A paused agent turn on `/v1/agents/{id}/execute` (the reply carries an `action_token`) | re-send the same call with `approved: true` + that `action_token` (both, or `400 invalid_params`) | nothing else | | A run whose `GET /v1/executions/{id}` reads `needs_approval` | `POST /v1/executions/{id}/continue` `{"approve": true, "allow_scope": "once"\|"run"\|"always"}` (MCP `plungeai_continue`) — the held action runs inside that call | **do NOT re-issue the original request** (it would act twice) | Canonical code list (all planes, every status): developer guide §13 at `GET /llms-full.txt` — §13.2 (execution-plane codes), §13.3 (outcome→HTTP mapping), §13.5 (the two trust-fence statuses). `403 refused` and `409 approval_required` are the fence; everything else is caller-fixable. Sync vs async and per-route detail are in each capability's own skill. ## Rate limits Execution-plane tiers: free 30/min · 1,000/day, pro 100/min · 10,000/day, enterprise 300/min · 100,000/day (a key with no tier counts as free; a per-key `rate_limit` override applies only on `https://mcp.plungeai.com/v1`, not here). Enforced per-key on `POST` calls only (`GET`s are free); a hit answers `429 rate_limited` with a `Retry-After` header — honour it. Failed authentications are metered per client IP. These numbers are the defaults: the operator sets the limits, and when they are switched off there is no `429` except failed-auth metering. The models plane has its own, separate limiter, default 600 req/min per key, answering `429 rate_limit_exceeded` with **no** `Retry-After` header — back off on a short fixed interval (~1s) instead. Don't hammer result-polling endpoints; use 2s+ intervals. ## Self-documenting endpoints (no auth), SDK and codegen `GET /v1/openapi.json` (OpenAPI 3.1) · `GET /llms.txt` (index) · `GET /llms-full.txt` (full guide and route reference as one markdown payload) · `GET /docs` (human page) · `GET /health`. AI agents at runtime can skip HTTP: connect an MCP client to `https://mcp.plungeai.com/v1` with an `ozk_` key (`plungeai-mcp-setup`). Typed clients: four official SDKs, generated from the live spec — TypeScript `@plungeai/one-api`, Python `plungeai`, Go `github.com/PlungeAI/plungeai-go`, Rust `plungeai` (setup and first call: `plungeai-sdk-setup`). For any other language, generate a client from `GET /v1/openapi.json`. Snippets (codegen, the SDK table, the curl cookbook, a paste-ready `AGENTS.md` block): [`references/sdk-and-codegen.md`](/skills/plungeai-api-setup/references/sdk-and-codegen). ## Verify ```bash # 1. Liveness (no auth) — expect {"status":"ok","service":"one-api-router",...} curl -s https://api.plungeai.com/health # 2. Authenticated GET — expect {"tools":[...],"count":N} curl -s "https://api.plungeai.com/v1/tools?limit=1" \ -H "Authorization: Bearer ozk_YOUR_KEY" # 3. A 401 here means the key is wrong; a 200 with cards means you're in: curl -s "https://api.plungeai.com/v1/discovery/search?q=web%20search&limit=1" \ -H "Authorization: Bearer ozk_YOUR_KEY" # 4. Who the key is — expect {"user_id":"…","via":"key","tier":…,"key_label":…} curl -s https://api.plungeai.com/v1/whoami -H "Authorization: Bearer ozk_YOUR_KEY" ``` For the models plane, verify with the same `ozk_` key: `curl -s https://api.plungeai.com/v1/models -H "Authorization: Bearer ozk_YOUR_KEY"`. ## References - [`references/bots-and-door.md`](/skills/plungeai-api-setup/references/bots-and-door) — bots, executions, schedules, triggers, connections, memory, skills, templates and the Agent Call door over REST: every route, body field, limit and status (One API 2.3.0). - [`references/route-map.md`](/skills/plungeai-api-setup/references/route-map) — every other operation, one line each: service, discovery, tools, agents, workflow execution, MCP, traces, models plane and the `/api/v1` drop-in. - [`references/sdk-and-codegen.md`](/skills/plungeai-api-setup/references/sdk-and-codegen) — the four official SDKs, typed-client generation for other languages, curl cookbook, self-documenting endpoints, the paste-ready `AGENTS.md` block for generated apps. ## Related skills - `plungeai-models` — the OpenAI-compatible plane in depth: routing, presets, guardrails, caching, pricing. - `plungeai-agents`, `plungeai-tools-connectors` — execute a registry agent or a structured tool; contracts, sync/async, 422 handling. - `plungeai-workflows` — CNL YAML authoring and the workflow execution routes (inline + saved, SSE streaming). - `plungeai-discovery` — catalog search, recommendations, cards. - `plungeai-scheduling` — `/v1/schedules` and `/v1/triggers`: cron, one-time `run_at`, wake, webhook/email/hook triggers. - `plungeai-bot-agent` — the saved bot's YAML and keys (what the REST bot routes govern). - `plungeai-results-traces` — `/v1/executions` (status, output, continue, followup, steer, files), traces, MCP-as-server routes. - `plungeai-mcp-setup` — the same platform over MCP instead of REST. - `plungeai-sdk-setup` — the official TypeScript, Python, Go and Rust SDKs. - `plungeai-cli-setup` — operate PlungeAI from a terminal. ## Reference pages <CardGroup cols={2}> <Card title="Bots, executions, schedules, connections and the Agent Call door — over REST" icon="file-text" href="/skills/plungeai-api-setup/references/bots-and-door"> One API 2.3.0 puts everything a saved bot needs on REST, under the same ozk key (Authorization: Bearer ozkYOURKEY). </Card> <Card title="Route map — the planes that are not bots, one line per operation" icon="file-text" href="/skills/plungeai-api-setup/references/route-map"> The One API 2.3.0 has 114 operations. </Card> <Card title="SDK & codegen — typed clients, snippets, and the AGENTS.md block" icon="file-text" href="/skills/plungeai-api-setup/references/sdk-and-codegen"> The supported integration stance, in order of preference: </Card> </CardGroup> # Bots, executions, schedules, connections and the Agent Call door — over REST Source: https://www.plungeai.com/docs/skills/plungeai-api-setup/references/bots-and-door <!-- sources-of-truth: orchestration/api-gateway/openapi-bots.ts, orchestration/api-gateway/openapi-executions.ts, orchestration/api-gateway/openapi-schedules.ts, orchestration/api-gateway/openapi-connections.ts, orchestration/api-gateway/routes/execute.ts, orchestration/api-gateway/routes/schedules.ts, orchestration/api-gateway/docs/guide-3.0/16-bots.md | last-synced: 2026-10-05 (One API 2.3.0; every field, limit and status checked against GET /v1/openapi.json) --> 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}`. ```bash 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. # Route map — the planes that are not bots, one line per operation Source: https://www.plungeai.com/docs/skills/plungeai-api-setup/references/route-map <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/discovery.ts, orchestration/api-gateway/routes/tools.ts, orchestration/api-gateway/routes/agents.ts, orchestration/api-gateway/routes/workflows.ts, orchestration/api-gateway/routes/mcp.ts, orchestration/api-gateway/routes/traces.ts, orchestration/api-gateway/routes/models-proxy.ts | last-synced: 2026-10-05 (One API 2.3.0, 114 operations, each field and status read from GET /v1/openapi.json) --> The One API 2.3.0 has 114 operations. Bots, executions, schedules, triggers, connections, memory, skills, templates and the Agent Call door are in `bots-and-door.md`; this file holds the rest, so every operation is named in this skill. Every route takes `Authorization: Bearer ozk_YOUR_KEY` except the three marked "no auth". Statuses below are the ones the spec lists per operation; `401 unauthorized` is on every authenticated route. Regenerate a client from `GET /v1/openapi.json` rather than copying this. ## Service | Route | Does | |---|---| | `GET /health` (no auth) | liveness `{status, service, version}` | | `GET /v1/openapi.json` (no auth) | this contract, OpenAPI 3.1 | | `GET /v1/whoami` | `{user_id, via: key\|internal, tier, key_label}` | ## Discovery (`plungeai-discovery`) | Route | Does | |---|---| | `GET /v1/discovery/search` | query `q`, `kind` (`agents`, `skills`, `digital-twins`, `personas`, `experts`, `backgrounds`, `workflows`, `models`, `providers`, `connectors`, `plugins`, `mcp-servers`), `type`, `category`, `status` (default `active`), `tier`, `limit`, `offset`, `mode` (`hybrid`, `keyword`, `vector`), `fields` (`list`, `summary`, `full`) → `{cards, count, total, searchMethod}` | | `POST /v1/discovery/recommend` | `{type, task}` → `{recommended, card, score}`; `400 invalid_json` / `invalid_request`, `429` | | `POST /v1/discovery/resolve` | `{task, kinds?, category?, status?}` → `{outcome: match\|uncertain\|none, tool, operation, call: {required_params, example_cnl}, confidence, alternatives, mode: shadow\|act}`; `400 invalid_json` | | `GET /v1/discovery/cards/{type}/{id}` | the card as markdown; `type` is singular (`agent`, `skill`, `digital_twin`, `expert`, `background`, `workflow`, `model`, `provider`, `connector`, `plugin`, `mcp_server`); `400 invalid_request`, `404 not_found` | ## Tools (`plungeai-tools-connectors`) | Route | Does | |---|---| | `GET /v1/tools` | active tool agents, `limit` ≤ 100 (default 50), `offset` → `{tools, count}` | | `GET /v1/tools/{id}` | the contract `{agent_id, credential, name, description, operations, inputSchema, examples, output_type}`; `404 unknown_tool` | | `POST /v1/tools/{id}/execute` | `{operation, params, prompt, format}` → `{ok, content, outcome, request_id}`; `403 refused`, `409 approval_required` or `duplicate_execution_id`, `413`, `422 invalid_params` (echoes the contract), `424` (with `connect`), `429`, `502 execution_failed`, `503 agent_unavailable` | ## Agents (`plungeai-agents`) | Route | Does | |---|---| | `GET /v1/agents` | active agents, `limit` ≤ 100 (default 50), `offset` → `{agents, count}` | | `GET /v1/agents/categories` | `{categories, count}` | | `POST /v1/agents/{id}/execute` | `prompt` (or `input`), `persona`, `provider`, `model`, `maxTokens` or `max_tokens`, `temperature`, `top_p`, `reasoning_effort`, `thinking_level`, `system`, `messages`, `sync` (default `true`), `stream` (default `false`: OpenAI-shaped SSE chunks, then `data: [DONE]`), `user_context`, `format`. `200 {content, workflow_id, task_id, request_id}`; `sync: false` is `202 {workflow_id, task_id, request_id}`. Errors: `400 missing_prompt`, `403 agent_not_active`, `404 unknown_agent`, `409 duplicate_execution_id`, `413`, `422 invalid_params` / `unknown_model` / `empty_completion` (raise `max_tokens` to 64 or more), `424`, `429`, `502 engine_error` / `result_unavailable`, `503 agent_unavailable` | | `GET /v1/agents/results/{workflowId}/{taskId}` | redeem an async result `{content, content_type, workflow_id, task_id}`; `404 not_ready` (poll, 2 s or more), `409 execution_failed`, `422 empty_completion` | ## Workflow execution (`plungeai-workflows` → `references/api.md`) `POST /v1/workflows/execute` (inline YAML or JSON), `POST /v1/workflows/{id}/execute` (saved), `POST /v1/workflows/execute-stream` and `POST /v1/workflows/{id}/execute-stream` (SSE), `GET /v1/workflows/results/{workflowId}/{taskId}` (redeem), `POST /v1/workflows/executions/{id}/cancel` (the same operation as `POST /v1/executions/{id}/cancel`). Bodies, events and errors are in that file; the saved-workflow CRUD and versions are in `bots-and-door.md`. ## MCP pass-through and outbound runs (`plungeai-mcp-setup`, `plungeai-results-traces`) | Route | Does | |---|---| | `GET /v1/mcp/tools` | the platform's MCP tool list (JSON-RPC `tools/list` result) | | `POST /v1/mcp` | Streamable-HTTP MCP pass-through: any MCP client with an `ozk_` key; JSON or SSE; a body that is not JSON is a JSON-RPC `-32700` error, not the router envelope | | `POST /v1/mcp/runs` | `{server_ids}` (non-empty string array) → `201 {run_id, tools, connected, failed, warnings}`; `502 no_servers_connected` | | `GET /v1/mcp/runs/{id}/tools` | `{tools, count}`; `404 run_not_found` once the run is closed or 30 minutes idle: open a new one | | `POST /v1/mcp/runs/{id}/call` | `{name, args?, timeout_ms?}` → `{content}`; `502 tool_call_failed` | | `DELETE /v1/mcp/runs/{id}` | close the run (idempotent) → `{success}` | ## Traces | Route | Does | |---|---| | `GET /v1/traces/{id}` | `{trace_id, spans, gateway_requests}` for the `x-trace-id` you sent; `404 not_found` for an unknown id or another key's (spans land asynchronously: retry in a few seconds) | ## Models plane (`plungeai-models`) Same `ozk_` key, `Authorization: Bearer` only. | Route | Does | |---|---| | `POST /v1/chat/completions` | `model`, `models[]` (ordered fallback), `sort` (`price`, `latency`, `throughput`), `messages`, `stream`, `temperature`, `max_tokens`, `top_p`; `@preset/<slug>` is a model name. `402 byok_required`, `403 model_not_allowed` / `content_blocked`, `404 preset_not_found` / `model_not_found`, `405 method_not_allowed`, `429 rate_limit_exceeded` / `spend_cap_exceeded`, `503 money_plane_unavailable` | | `POST /v1/embeddings` | `{model, input}` | | `POST /v1/completions` | legacy `{model, prompt (one string), max_tokens?, stream?}`, answered as `text_completion` | | `GET /v1/models` | the priced catalog | | `GET /v1/models/{author}/{slug}/endpoints` | the endpoint that serves one model, with per-token pricing | | `GET /v1/generation?id=` | usage and cost of one completion (`gen-…` id or `x-request-id`) | | `GET /v1/credits` | `{data: {total_credits, total_usage}}` in USD | | `GET /v1/key` | the calling key: label, monthly limit, usage, rate limit | The OpenRouter drop-in base `/api/v1` serves the same eight operations (`POST /api/v1/chat/completions`, `/embeddings`, `/completions`; `GET /api/v1/models`, `/generation`, `/credits`, `/key`, `/models/{author}/{slug}/endpoints`) with OpenRouter-shaped errors (`{"error":{"code": <number>, "message", "metadata"}}`). Point an OpenRouter client's base URL there; new code uses `/v1`. # SDK & codegen — typed clients, snippets, and the AGENTS.md block Source: https://www.plungeai.com/docs/skills/plungeai-api-setup/references/sdk-and-codegen <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/discovery.ts, orchestration/api-gateway/routes/workflows.ts, orchestration/api-gateway/lib/respond.ts, orchestration/api-gateway/docs/guide-3.0/15-sdks-and-clients.md, sdk/one-api-client/README.md, sdk/one-api-python/README.md, sdk/one-api-go/README.md, sdk/one-api-rust/README.md, docs/ONE-API-DEVELOPER-GUIDE-2.0.md | last-synced: 2026-10-14 (One API 2.5.0, 136 operations; the four SDKs 1.0.0 ship via the `one-api-v1.0.0`, `one-api-python-v1.0.0`, `one-api-go-v1.0.0` and `one-api-rust-v1.0.0` tags) --> The supported integration stance, in order of preference: 1. **Plain HTTPS** — every plane is JSON over HTTPS; any HTTP client works. 2. **The OpenAI SDK you already use** — for the models plane only (`base_url = https://api.plungeai.com/v1`, your `ozk_` key). See the `plungeai-models` skill. 3. **The official SDKs** — TypeScript `@plungeai/one-api`, Python `plungeai`, Go `github.com/PlungeAI/plungeai-go`, Rust `plungeai`. Setup and first call: the `plungeai-sdk-setup` skill. 4. **Generate your own client** from the live OpenAPI 3.1 spec — for any other language, or when you want only the types. The OpenAPI document at `GET /v1/openapi.json` is the contract; the official SDKs' resource layers are generated from it, so every operation in the spec has a method in each of them. ## Generate your own client (any language) The spec is OpenAPI 3.1 at `https://api.plungeai.com/v1/openapi.json`. Any 3.1-capable generator works. **TypeScript**: ```bash npx openapi-typescript https://api.plungeai.com/v1/openapi.json -o src/plungeai-schema.d.ts npm install openapi-fetch ``` ```ts import createClient from 'openapi-fetch' import type { paths } from './plungeai-schema' export const plungeai = createClient<paths>({ baseUrl: 'https://api.plungeai.com', headers: { Authorization: `Bearer ${process.env.PLUNGEAI_API_KEY}` }, }) ``` Re-run the generator when the API changes shape — never patch generated types by hand. Every operation carries a stable `operationId` (`post-v1-workflows-id-execute`, `get-v1-schedules-id-runs`, …) for generators that name methods from it. One thing to know: `/v1/discovery/recommend` and `/v1/discovery/cards/{type}/{id}` normalize the registry's own flat `{"error":"<string>"}` rejections into the standard envelope (`{"error":{"code","message"}}`, `code` is `not_found` on a 404, `invalid_request` otherwise) before you ever see them — a generated client's error type for these two routes is accurate as-is, no special-casing needed. **Python** (prefer the official `plungeai` package) — for a generated client: `openapi-python-client generate --url https://api.plungeai.com/v1/openapi.json`. ## Official SDKs Four packages, one shape: the client reads `PLUNGEAI_API_KEY`, one method per operation (`client.agents.execute(…)`), typed errors by status (424 carries `connect.link`), 2 retries with backoff on connection errors, 408, 429 and ≥ 500 — never on 409 or 424 — a 10-minute timeout, SSE streams, auto-iterating pages, raw-response access, and an idempotency key sent as `x-trace-id`. All target One API 2.5.0, package version 1.0.0. | Language | Package | Install | Until published, build from | |---|---|---|---| | TypeScript | `@plungeai/one-api` (npm) | `npm install @plungeai/one-api` | `sdk/one-api-client/` (tag `one-api-v1.0.0`) | | Python | `plungeai` (PyPI) | `pip install plungeai` | `sdk/one-api-python/` (tag `one-api-python-v1.0.0`) | | Go | `github.com/PlungeAI/plungeai-go` | `go get github.com/PlungeAI/plungeai-go` | `sdk/one-api-go/` (tag `one-api-go-v1.0.0`) | | Rust | `plungeai` (crates.io) | `cargo add plungeai` | `sdk/one-api-rust/` (tag `one-api-rust-v1.0.0`) | None is on its registry yet: until its tag is published, build from the `sdk/` directory in the platform repository. First call in each language, discovery, and the per-language detail: the `plungeai-sdk-setup` skill. ## curl cookbook Agent/tool ids below (`brave-agent`, `one-agent`) are illustrative — take real ids from the discovery call two lines up, never from memory. ```bash BASE=https://api.plungeai.com AUTH='Authorization: Bearer ozk_YOUR_KEY' curl -s $BASE/health # liveness, no auth curl -s $BASE/v1/openapi.json | head -50 # live contract, no auth curl -s "$BASE/v1/discovery/search?q=web%20search&limit=3" -H "$AUTH" curl -s "$BASE/v1/tools?limit=5" -H "$AUTH" curl -s $BASE/v1/tools/brave-agent -H "$AUTH" # contract (id from discovery) curl -s -X POST $BASE/v1/agents/one-agent/execute -H "$AUTH" \ -H 'Content-Type: application/json' -d '{"prompt":"Say: ok"}' curl -s $BASE/v1/traces/REQUEST_ID_FROM_ABOVE -H "$AUTH" curl -s "$BASE/v1/workflows?kind=bot&limit=5" -H "$AUTH" # your saved bots curl -s -X POST $BASE/v1/execute -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"agent":"<bot id>@1","input":"hi","dry_run":true}' # the Agent Call door, planned not run curl -s "$BASE/v1/executions?limit=3" -H "$AUTH" ``` ## Self-documenting endpoints The API teaches itself from the domain — useful for onboarding a coding agent with zero prior context: - `GET https://api.plungeai.com/llms.txt` — index (llmstxt.org format) - `GET https://api.plungeai.com/llms-full.txt` — the full developer guide + route reference as one markdown payload - `GET https://api.plungeai.com/v1/openapi.json` — the machine contract - `GET https://api.plungeai.com/docs` — human-readable docs page AI agents at **runtime** can skip HTTP clients entirely: connect an MCP client to `https://mcp.plungeai.com/v1` with an `ozk_` key (see the `plungeai-mcp-setup` skill). ## AGENTS.md block for generated apps When you generate an app that calls PlungeAI, drop this block into the app's `AGENTS.md` (or `CLAUDE.md`) so future coding agents in that repo handle the API correctly. Paste-ready: ```markdown ## PlungeAI One API This app calls the PlungeAI One API at https://api.plungeai.com. Official SDKs (env `PLUNGEAI_API_KEY`): `@plungeai/one-api`, `plungeai` (PyPI), `github.com/PlungeAI/plungeai-go`, `plungeai` (crates.io). - Auth: `Authorization: Bearer $PLUNGEAI_API_KEY` (an `ozk_` platform key) for /v1/discovery, /v1/tools, /v1/agents, /v1/workflows, /v1/executions, /v1/schedules, /v1/triggers, /v1/bot-groups, /v1/channels, /v1/connections, /v1/memory, /v1/skills, /v1/templates, /v1/execute, /v1/mcp, /v1/traces. The OpenAI-compatible models plane (/v1/chat/completions, /v1/embeddings, /v1/models) takes the same key, as `Authorization: Bearer` only. Keys live in env vars only — never commit or log them. - Live contract: `GET /v1/openapi.json`. Live catalog: `GET /v1/agents`, `GET /v1/discovery/search?q=...`. Never hardcode agent/tool/model ids or counts — discover them, and re-discover on 404 `unknown_tool` / `workflow_not_found`. - Sync vs async: agent execute defaults to sync; `sync:false` returns 202 + `{workflow_id, task_id}` — poll `GET /v1/agents/results/:wf/:task` (404 `not_ready` is normal in-flight, back off 2s+). - HARD RULE — trust fences: `403 refused` (a gated/money verb refused outright) and `409 approval_required` (a human must approve before the action runs — this is implemented and produced today, not a placeholder) are both final for now — surface them to the user; never retry, rephrase, or route around a `403 refused`. For `409`, a human approves, by shape: a 409 from a tool call → Ocean Studio, then re-issue the identical request; a paused agent turn → re-send with `approved: true` + its `action_token`; a run reading `needs_approval` → `POST /v1/executions/{id}/continue` `{"approve": true}` (MCP `plungeai_continue`) — the held action runs inside that call, so do NOT re-issue the original request afterwards. A separate, unrelated `403 agent_not_active` (a parked/inactive agent id) is a lifecycle state, not a fence — re-discover a fresh id and retry with that instead. - `422 invalid_params` echoes the tool contract — self-correct once, then stop. - Errors carry `X-Error-Code`; branch on it. Send a fresh UUID as `x-trace-id` (a reused one is `409 duplicate_execution_id`). Bots, schedules, executions and the `POST /v1/execute` door: `GET /llms-full.txt` §16. - Rate limits: execution-plane `429 rate_limited` carries `Retry-After` — honour it. The models plane's separate `429 rate_limit_exceeded` has no Retry-After — use a fixed ~1s backoff there instead. - 502 `upstream_error` / 500 `internal_error` can come from any route — retry once with backoff. - Correlation: send `x-trace-id` on writes; debug via `GET /v1/traces/:id`. - Full docs for agents: `GET https://api.plungeai.com/llms-full.txt`. ``` Keep the block verbatim except the env-var names, which should match the app's conventions. # plungeai-cli-setup Source: https://www.plungeai.com/docs/skills/plungeai-cli-setup Install and log in to the PlungeAI Ocean CLI (ocean-cli, npm package @plungeai/ocean) and run your first commands. Use when a user or agent wants PlungeAI from a terminal, CI or a shell-first coding agent and does not have ocean-cli working yet; hand over to the ocean-cli skill for the full verb reference. [Download zip](https://skills.plungeai.com/plungeai-cli-setup.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-cli-setup/SKILL.md) `ocean-cli` is PlungeAI's non-interactive command-line client — the CLI door next to the MCP server (`https://mcp.plungeai.com/v1`) and the One API (`https://api.plungeai.com/v1/openapi.json`). It runs nothing locally: every command calls the deployed platform. Full verb reference: the `ocean-cli` skill. ## Install (Node ≥ 20) ```bash node --version # v20 or newer npm install -g @plungeai/ocean # once published (see below); or: npx @plungeai/ocean --help ocean-cli --version # ocean-cli, version 2.x ``` `@plungeai/ocean` is not on npm yet: until the `ocean-v2.1.0` tag is published, run it from the repository (`npm run build:ocean-cli` + `node apps/ocean-cli/dist/index.js`). After the publish, `curl -fsSL https://plungeai.com/install.sh | sh` is the alternative (checks Node, runs the npm install, prints the login line); before it, that script ends at npm's 404. From a clone of Ocean-Platform: `npm run ocean-cli -- --help`. Update later with `ocean-cli update`. ## Log in — one key, one optional session | Credential | Get it | Store it | Unlocks | |---|---|---|---| | API key `ozk_…` | `ocean-cli login` opens the browser at https://dashboard.plungeai.com (any sign-in method); or One API → Keys | `ocean-cli login` (browser) · `ocean-cli login --no-browser` (hidden prompt, for SSH) | search, research, agents, tools, workflows, bots, executions, missions, schedules, memory, discovery, chat, models, traces | | Studio session | Studio → your `auth-session` cookie value | `ocean-cli login --session` | keys, balance, usage, nodes, campaigns, connections, secrets, documents, teams, projects, `executions export` | Never pass a credential as an argument. In CI use `--key -` on stdin, or set `OCEAN_API_KEY` / `OCEAN_SESSION` (environment values win for one process and are never written to disk). Credentials live in `~/.ocean/config.json` (mode 0600). ## Verify ```bash ocean-cli auth --json # {"authenticated": true, "method": "api_key", "user_id": "…", "tier": "…", "session": false, …} ocean-cli doctor # ✓ API / MCP / Studio reachable, ✓ each stored credential valid — exit 4 lists what is missing ocean-cli whoami # identity, tier, rate window ``` ## First three commands ```bash ocean-cli search "Who is the current Head of Sales at Adyen?" --max-results 3 ocean-cli discovery search "web search" --kind agents --limit 5 --json # discover agents live — never guess an id ocean-cli agents run one-agent "Reply with exactly: hello from ocean-cli" --json ``` ## Conventions you can rely on `ocean-cli <noun> <verb> [ARGS] [--flags]`; `--json` on every verb; `--help` on every level; exit codes 0 ok · 2 usage · 3 auth · 4 refused/not ready/rate-limited · 5 timeout; long operations take `--no-wait` and a `poll` verb; destructive verbs need `--yes`; `--dry-run` shows a paid or creating call without sending it; `-` reads a text argument from stdin; results of `research`, `findall`, `missions` and `enrich` are saved to files. Shell completion: `ocean-cli completion install`. The PlungeAI skills themselves: `ocean-cli skills install`. ## Slash commands in your coding agent The PlungeAI plugin ships `/ocean-*` commands (`/ocean-run`, `/ocean-create`, `/ocean-runs`, `/ocean-schedule`, …) for Claude Code and OceanCode that call `ocean-cli` for you — see `plungeai-in-claude-code`. `/ocean` lists them. ## Reference pages <CardGroup cols={2}> <Card title="ocean-cli — flows an agent runs end to end" icon="file-text" href="/skills/plungeai-cli-setup/references/agent-flows"> Skill: plungeai-cli-setup. Replace <…> with real values; confirm any flag with ocean-cli <noun> <verb> --help. </Card> <Card title="ocean-cli — install, credentials, config, doctor" icon="file-text" href="/skills/plungeai-cli-setup/references/auth-and-setup"> Skill: plungeai-cli-setup. The full verb tree is in the ocean-cli skill (references/verbs.md). </Card> <Card title="ocean-cli — the nouns" icon="file-text" href="/skills/plungeai-cli-setup/references/commands"> Grammar: ocean-cli <noun> <verb> [ARGS] [--flags]. </Card> </CardGroup> # ocean-cli — flows an agent runs end to end Source: https://www.plungeai.com/docs/skills/plungeai-cli-setup/references/agent-flows <!-- sources-of-truth: apps/ocean-cli/src/commands/workflows.ts apps/ocean-cli/src/commands/executions.ts apps/ocean-cli/src/commands/missions.ts --> Skill: `plungeai-cli-setup`. Replace `<…>` with real values; confirm any flag with `ocean-cli <noun> <verb> --help`. ## Create, validate, save, run ```bash ocean-cli workflows build --goal "watch AI funding news and email me daily" --json # the platform drafts the YAML # save the returned YAML to flow.yaml, then: ocean-cli workflows validate flow.yaml # CNL schema + save-time policy lint, runs locally ocean-cli workflows create "AI funding watch" flow.yaml --json # prints the workflow id ocean-cli workflows run <workflow_id> -i "AI startups" --json # waits for the result ``` Change a saved workflow: `ocean-cli workflows build --workflow-id <id> --instruction "add a summary step" --json`, validate the new YAML, then `ocean-cli workflows update <id> -f flow.yaml`. History: `workflows versions <id>`, `workflows save-version <id> --label "<why>"`, `workflows restore <id> <version_id> --yes`. ## Run without waiting, then poll ```bash ocean-cli workflows run <workflow_id> -i "<input>" --no-wait --json # prints execution_id ocean-cli executions poll <execution_id> --timeout 900 --json # waits, then prints the result ocean-cli executions output <execution_id> # the platform markdown ocean-cli executions conversation <execution_id> --json # the full thread ``` `workflows run <file.yaml>` runs a YAML file inline, without saving it. A paused run (a question or an approval) shows up in `ocean-cli executions status <id>`; answer with `executions continue <id> --message "…"` or `--approve`. A running run takes `executions steer <id> "<text>"` at its next turn. ## Bounded mission (`type: harness`) ```bash ocean-cli missions run "<goal>" --tools web_search,web_fetch,task_complete --max-iterations 5 --no-wait --json ocean-cli missions poll <id> --timeout 600 -o mission.json --json # saves the result file ocean-cli executions output <id> ``` `missions run … --sync` answers on one connection instead of start + poll. Results are saved under `./ocean-missions/<id>.json` unless `-o` says otherwise; an existing file is never overwritten without `--force`. ## CI Set `OCEAN_API_KEY` from a repository secret (never echo it), run the verb with `--json`, and let the exit code decide the job: 0 ok, 2 bad input, 3 auth, 4 API/refused/not ready, 5 timeout. # ocean-cli — install, credentials, config, doctor Source: https://www.plungeai.com/docs/skills/plungeai-cli-setup/references/auth-and-setup <!-- sources-of-truth: apps/ocean-cli/README.md apps/ocean-cli/src/commands/account.ts apps/ocean-cli/src/runtime/session.ts --> Skill: `plungeai-cli-setup`. The full verb tree is in the `ocean-cli` skill (`references/verbs.md`). ## Install - `@plungeai/ocean` is not on npm yet: until the `ocean-v2.1.0` tag is published, run it from the repository (`npm run build:ocean-cli` + `node apps/ocean-cli/dist/index.js`). - After the publish: `npm install -g @plungeai/ocean` (Node ≥ 20), then `ocean-cli --help`; without installing: `npx @plungeai/ocean --help`. - Installer: `curl -fsSL https://plungeai.com/install.sh | sh` checks Node, runs the npm install and prints the login line (it fails at npm's 404 until the publish). - From a clone of the Ocean-Platform repository: `npm run ocean-cli -- --help` (TypeScript source via tsx), or `npm run build:ocean-cli` and `node apps/ocean-cli/dist/index.js --help` for the bundle. - `ocean-cli --version` prints `ocean-cli, version 2.x`. `ocean-cli update --check` compares it with the npm registry; `ocean-cli update` installs the latest version. ## Credentials | Credential | Used by | Store it | Environment override | |---|---|---|---| | API key `ozk_YOUR_KEY` | web verbs, agents, tools, workflows, bots, executions, missions, schedules, memory, learn, templates, chat, discovery, models, mcp, traces | `ocean-cli login` | `OCEAN_API_KEY` (or `PLUNGEAI_API_KEY`) | | Studio session (the `auth-session` cookie value) | keys, balance, usage, nodes, campaigns, connections, secrets, documents, teams, projects, `executions export` | `ocean-cli login --session` | `OCEAN_SESSION` | | Legacy inference key `sk-ocean-…` | `models list\|chat\|embed`, only when no API key is stored | hidden option `ocean-cli login --inference` | `OCEAN_INFERENCE_KEY` | - `ocean-cli login` opens your browser at https://dashboard.plungeai.com. Sign in there (any method) and the CLI receives your account key through a one-time code sent to `127.0.0.1`, verifies it and stores it. The key never appears in a browser URL. If no sign-in arrives within 3 minutes, the command exits 5. - `--no-browser` asks for a pasted key at a hidden prompt instead (create one at One API → Keys) — use it over SSH. `--key -` reads the key from stdin and `--key FILE` from a file; neither opens a browser, and `--session` never does. A secret is never accepted as a command-line argument. - In a terminal the first verb that needs the key asks for it; under `--json`, in a pipe or in CI it exits 3 instead. - `ocean-cli logout` clears the credential fields and nothing else. - Credentials live in `~/.ocean/config.json` (mode 0600). `OCEAN_CLI_CONFIG_DIR` points the CLI at another directory, which isolates CI runners. Environment credentials win for one process and are never written to disk. - Each credential goes only to its own hosts: the API key to `https://api.plungeai.com` (`X-API-Key`) and `https://mcp.plungeai.com/v1` (`Authorization: Bearer`); the session cookie to `https://studio.plungeai.com` and `https://dashboard.plungeai.com`. ## Config `ocean-cli config [KEY] [VALUE]` views or sets `api_url`, `mcp_url`, `studio_url`, `dashboard_url` and `default_provider` (the search agent: `parallel`, `tavily`, `firecrawl`, `serper`, `brave` or `exa`). Other environment variables: `OCEAN_CLI_QUIET=1` (silence status lines), `OCEAN_CLI_TIMEOUT_MS` (request limit, default 120000). ## Verify with `ocean-cli doctor` Up to six live checks (the three hosts always; the two key checks when an API key is stored; the Studio session check when a session is stored); it exits 4 when any fails and names the fix: ``` ✓ API reachable: https://api.plungeai.com 200 ✓ MCP reachable: https://mcp.plungeai.com 200 ✓ Studio reachable: https://studio.plungeai.com 200 ✓ API key valid: discovery search 200 ✓ MCP key valid: <user id> (<tier>) ✓ Studio session valid: workflows 200 ``` A ✗ on a key line: run `ocean-cli login` again. A ✗ on the Studio session line: `ocean-cli login --session` (only the Studio verbs need it). `ocean-cli auth --json` prints `{authenticated, method, token_file, user_id, tier, session, inference_key}` with `method` one of `env`, `api_key`, `none`. `ocean-cli whoami` shows the identity, tier and rate window; the free tier allows 30 requests per minute. ## Exit codes | Code | Class | When | |---|---|---| | 0 | ok | success, help, version | | 2 | bad input | a bad flag or argument, invalid JSON, a missing `--yes`, a file that does not exist | | 3 | auth | no credential for the surface, HTTP 401/403 | | 4 | API | any other non-2xx, a tool outcome of error/needs approval, a rate limit, a refusal by the unattended fence (`approval_required`) | | 5 | timeout | `--timeout` reached while waiting, or a request timed out | With `--json` an error is one JSON object on stderr: `{"error":{"code":"…","message":"…","status":…}}`. # ocean-cli — the nouns Source: https://www.plungeai.com/docs/skills/plungeai-cli-setup/references/commands <!-- sources-of-truth: apps/ocean-cli/src/index.ts apps/ocean-cli/src/commands/ | full tree: apps/ocean-skills/skills/ocean-cli/references/verbs.md --> Grammar: `ocean-cli <noun> <verb> [ARGS] [--flags]`. Every flag: see the `ocean-cli` skill's `references/verbs.md` (generated from `--help`), or run `ocean-cli <noun> <verb> --help` (every leaf carries an example). ## Account (no noun) | Command | What it does | |---|---| | `login`, `logout`, `auth` | Sign in (browser by default), clear, and inspect credentials | | `whoami`, `doctor` | Identity, tier and rate window; live checks of every plane | | `config [key] [value]` | View or set the host URLs and the default search provider | | `update [--check]`, `completion show\|install`, `skills list\|install\|uninstall` | Self-update, shell completion, PlungeAI agent skills | ## Web (API key) | Command | What it does | |---|---| | `search`, `extract`, `answer` | Web search, clean markdown from URLs, a cited answer | | `research run\|status\|poll\|result\|processors` | Deep research (paid, minutes) | | `findall run\|status\|poll\|result\|cancel\|ingest\|entity-search\|extend\|enrich\|schema` | Discover entities from the web | | `monitor create\|list\|get\|update\|cancel\|trigger\|events` | Track the web for changes (`create` is refused by the unattended fence) | | `enrich run\|suggest\|status\|poll` | Add web-researched columns to CSV or JSON rows | ## Platform (API key) | Command | What it does | |---|---| | `agents list\|categories\|get\|run\|result` | Registry agents | | `tools list\|get\|call` | Tool agents: contracts and typed calls | | `workflows list\|get\|create\|update\|delete\|versions\|restore\|validate\|run\|result\|cancel\|build\|save-version` | Saved CNL workflows (`--kind bot` for bots) | | `workflows permissions get\|set`, `files`, `inbox`, `bindings list\|bind\|unbind` | A bot's policy, workspace files, inbox and bound chats | | `bot-groups list\|create\|update\|delete\|thread` | Named sets of 2 to 6 bots | | `executions list\|get\|conversation\|cancel\|output\|delete\|status\|poll\|continue\|followup\|steer\|files\|export` | Runs | | `missions run\|status\|poll\|result` | Bounded autonomous runs with memory (`type: harness`) | | `schedules stats\|list\|get\|runs\|pause\|resume\|run-now\|delete\|create\|update\|wake` | Cron, one-time and ending jobs | | `schedules triggers webhook\|email\|hooks mint\|list\|delete` | Inbound webhook and email triggers, outbound signed run hooks | | `memory recall\|remember\|search-runs\|get-run`, `learn add\|list\|forget`, `templates list\|get\|use` | Memory, learned skills, templates | | `chat send\|new\|sessions\|history` | Platform chat turns | | `discovery search\|recommend\|card`, `models list\|chat\|embed`, `mcp tools\|runs …`, `traces get` | The registry, the models plane, MCP, traces | ## Studio session `balance`, `usage`, `keys list\|create\|revoke`, `nodes …`, `campaigns …`, `connections …`, `secrets …`, `documents …`, `teams …`, `projects …`, `executions export` — all need `ocean-cli login --session`. # plungeai-sdk-setup Source: https://www.plungeai.com/docs/skills/plungeai-sdk-setup Install and use the official PlungeAI One API SDKs — TypeScript `@plungeai/one-api` (npm), Python `plungeai` (PyPI), Go `github.com/PlungeAI/plungeai-go` and Rust `plungeai` (crates.io) — for the One API at `https://api.plungeai.com`: key from PLUNGEAI_API_KEY, first whoami and agents.execute call, discovery-first lookups, typed errors, retries, streams, pages and idempotency. Use when a user or agent writes code (an app, a script, a backend, a generated project) that calls PlungeAI in TypeScript, Python, Go or Rust and wants a typed client instead of raw HTTP, or asks "is there a PlungeAI SDK", "pip install plungeai", "npm @plungeai/one-api", "PlungeAI Go/Rust client". Every SDK method is generated from the live contract at `https://api.plungeai.com/v1/openapi.json`. Wire-level API and curl: `plungeai-api-setup`; models plane with the OpenAI SDK: `plungeai-models`; terminal: `plungeai-cli-setup`; MCP: `plungeai-mcp-setup`. [Download zip](https://skills.plungeai.com/plungeai-sdk-setup.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-sdk-setup/SKILL.md) Four official SDKs for the One API (`https://api.plungeai.com`), one shape: a client that reads the key from `PLUNGEAI_API_KEY`, one method per operation generated from `https://api.plungeai.com/v1/openapi.json` (every route in the spec has a method), typed errors, retries, streams and pages. All four target One API 2.5.0, package version 1.0.0. The wire-level API (curl, headers, error codes): `plungeai-api-setup`. ## Pick a language | Language | Package | Install | Runtime deps | |---|---|---|---| | TypeScript | `@plungeai/one-api` (npm) | `npm install @plungeai/one-api` | none (ESM + CJS; Node 20+, Deno, Bun, Workers) | | Python | `plungeai` (PyPI) | `pip install plungeai` | `httpx`; Python ≥ 3.10; sync `PlungeAI` + `AsyncPlungeAI` | | Go | `github.com/PlungeAI/plungeai-go` | `go get github.com/PlungeAI/plungeai-go` | standard library only; Go ≥ 1.24 | | Rust | `plungeai` (crates.io) | `cargo add plungeai tokio --features tokio/full` | `reqwest` (rustls) on `tokio`; Rust ≥ 1.85 | None of the four is on its registry yet. Until its tag is published, build from `sdk/<dir>/` in the platform repository: - `@plungeai/one-api` is not on npm yet: until the `one-api-v1.0.0` tag is published, build from `sdk/one-api-client/` (`npm ci && npm run build`). - `plungeai` is not on PyPI yet: until the `one-api-python-v1.0.0` tag is published, build from `sdk/one-api-python/` (`bash scripts/build.sh`, then `pip install target/sdk-py-build/*.whl`). - `github.com/PlungeAI/plungeai-go` is not on the Go proxy yet: until the `one-api-go-v1.0.0` tag is published, `replace` it with `sdk/one-api-go/`. - `plungeai` is not on crates.io yet: until the `one-api-rust-v1.0.0` tag is published, depend on the path `sdk/one-api-rust/`. Release check: `npm view @plungeai/one-api version` · `pip index versions plungeai` · `go list -m -versions github.com/PlungeAI/plungeai-go` · `cargo search plungeai`. ## Key One `ozk_` key for every plane — create it in **Dashboard → One API → Keys** (https://dashboard.plungeai.com). Export it as `PLUNGEAI_API_KEY`; every SDK reads that variable (the CLI too). The curl examples in the other skills use `$PLUNGE_API_KEY` — same key, different variable name. Never hardcode it, log it, or ship it to a browser (the TypeScript client refuses browsers unless you pass `dangerouslyAllowBrowser: true`). ```bash export PLUNGEAI_API_KEY=ozk_YOUR_KEY ``` ## First call TypeScript: ```ts import PlungeAI from '@plungeai/one-api' const client = new PlungeAI() const me = await client.whoami() const run = await client.agents.execute('one-agent', { prompt: 'Reply with exactly the word: pong', max_tokens: 32 }) ``` Python: ```python from plungeai import PlungeAI client = PlungeAI() # or AsyncPlungeAI() — same methods, awaited me = client.whoami() run = client.agents.execute("one-agent", prompt="Reply with exactly the word: pong", max_tokens=32) ``` Go: ```go import "github.com/PlungeAI/plungeai-go" client := plungeai.NewClient() // plungeai.WithAPIKey(…) to override PLUNGEAI_API_KEY ctx := context.Background() me, err := client.Whoami(ctx) run, err := client.Agents.Execute(ctx, "one-agent", plungeai.AgentExecuteParams{Prompt: "Reply with exactly the word: pong", MaxTokens: plungeai.Int(32)}) ``` Rust: ```rust use plungeai::{types::*, Client}; let client = Client::new()?; // or Client::builder().api_key(…).build()? let me = client.whoami().await?; let run = client .agents() .execute("one-agent", &AgentExecuteParams { prompt: Some("Reply with exactly the word: pong".into()), max_tokens: Some(32), ..Default::default() }) .await?; ``` ## Discover before you call `one-agent` above is the general-purpose agent; every other agent, tool or workflow id comes from a live lookup, never from memory. Discovery is one SDK call (`GET /v1/discovery/search` on the wire): ```ts const hits = await client.discovery.search({ q: 'web search', kind: 'agents', limit: 3 }) // TypeScript ``` ```python hits = client.discovery.search(q="web search", kind="agents", limit=3) # Python ``` ```go hits, err := client.Discovery.Search(ctx, plungeai.DiscoverySearchParams{Q: "web search", Kind: "agents", Limit: plungeai.Int(3)}) ``` ```rust let hits = client.discovery().search(&DiscoverySearchParams { q: Some("web search".into()), kind: Some(DiscoverySearchParamsKind::Agents), limit: Some(3), ..Default::default() }).await?; ``` Then read the tool contract (`GET /v1/tools/{id}`; `client.tools.retrieve(id)` in TypeScript) before executing. A 404 on an id means re-discover, not retry. ## What every SDK gives you | Concern | Behaviour (TS · Python · Go · Rust) | |---|---| | Errors | One class per status — TS/Python `BadRequestError` … `NotFoundError`, `ConflictError` (409), `FailedDependencyError` (424, carries `connect.link` — where to connect the missing account), `RateLimitError`, `InternalServerError` (≥ 500); Go one `*plungeai.Error` with `StatusCode`, `Code`, `RequestID`, `Connect`; Rust `Error::Api(ApiError)` with `status`, `code`, `request_id`, `connect` | | Retries | 2 by default, exponential backoff honouring `Retry-After`, on connection errors, timeouts, 408, 429, ≥ 500. **Never on 409 or 424** — those are semantic answers (`approval_required`, `duplicate_execution_id`, a missing connection); surface them | | Timeout | 10 minutes per request, configurable on the client and per call | | Streams | SSE: `workflows.executeInlineStream` / `execute_inline_stream` / `ExecuteInlineStream`; `stream: true` operations have a streaming form (`stream=True`, `…Streaming`, `…_stream`) | | Pages | Offset pages; `for await` / `for` / `…AutoPaging` / `into_stream()` walk every page | | Raw response | TS `.withResponse()` / `.asResponse()` · Python `with_raw_response` · Go `WithResponseInto(&raw)` · Rust `…_raw`; plus `get/post/…` escape hatches | | Idempotency | `idempotencyKey` · `idempotency_key` · `WithIdempotencyKey` · `RequestOptions::idempotency_key` → sent as `x-trace-id`, the run's execution id; a replay answers 409 `duplicate_execution_id` instead of starting twice | Per-language detail (async client, streaming, pagination, error tables, options forms, raw responses): [`references/per-language.md`](/skills/plungeai-sdk-setup/references/per-language). ## Hand-over - `plungeai-api-setup` — the wire-level One API: auth headers, curl, error codes, codegen for other languages. - `plungeai-agents` — agent execution in depth (sync/async, results polling, trust fences). - `plungeai-workflows` — CNL workflows: author, save, run, stream. - `plungeai-models` — the OpenAI-wire models plane; the OpenAI SDK also works there with base URL `https://api.plungeai.com/v1`. - `plungeai-cli-setup` — the same platform from a terminal or CI. - `plungeai-mcp-setup` — the same platform as an MCP server for a live agent. ## Reference pages <CardGroup cols={2}> <Card title="Per-language detail — the four One API SDKs" icon="file-text" href="/skills/plungeai-sdk-setup/references/per-language"> Shared by all four: the key comes from PLUNGEAIAPIKEY; base URL `https://api.plungeai.com`; retries 2 with exponential backoff honouring Retry-After on… </Card> </CardGroup> # Per-language detail — the four One API SDKs Source: https://www.plungeai.com/docs/skills/plungeai-sdk-setup/references/per-language <!-- sources-of-truth: sdk/one-api-client/README.md sdk/one-api-python/README.md sdk/one-api-go/README.md sdk/one-api-rust/README.md orchestration/api-gateway/docs/guide-3.0/15-sdks-and-clients.md | last-synced: 2026-10-14 (One API 2.5.0, 136 operations; SDKs 1.0.0) --> Shared by all four: the key comes from `PLUNGEAI_API_KEY`; base URL `https://api.plungeai.com`; retries 2 with exponential backoff honouring `Retry-After` on connection errors, timeouts, 408, 429 and ≥ 500 — never on 409 or 424 (semantic answers: `approval_required`, `duplicate_execution_id`, a missing connection; a retried execute would start a second run); 10-minute timeout; the idempotency key is sent as `x-trace-id`, becomes the run's execution id, is re-sent on every retry of the call, and a replay answers 409 `duplicate_execution_id`. Agent and tool ids below (`calculator-agent`) are illustrative — discover real ids with `discovery.search` first. Error statuses map the same way in TypeScript and Python: | Status | Error class | |---|---| | 400 | `BadRequestError` | | 401 | `AuthenticationError` | | 403 | `PermissionDeniedError` | | 404 | `NotFoundError` | | 409 | `ConflictError` | | 422 | `UnprocessableEntityError` | | 424 | `FailedDependencyError` — `connect: { provider, method, link }` says where to connect the missing account | | 429 | `RateLimitError` | | ≥ 500 | `InternalServerError` | | no response | `APIConnectionError` (timeout: TS `APIConnectionTimeoutError`, Python `APITimeoutError`) | `code` is the envelope's `error.code`; the request id is the `x-request-id` header. ## TypeScript — `@plungeai/one-api` ```ts import PlungeAI from '@plungeai/one-api' const client = new PlungeAI({ apiKey: process.env['PLUNGEAI_API_KEY'] }) // the default; can be omitted const stream = await client.workflows.executeInlineStream({ workflow }) // workflow: CNL YAML string for await (const event of stream) { if (event.event === 'token') process.stdout.write((event.data as { delta: string }).delta) } for await (const w of client.workflows.list({ kind: 'bot' })) console.log(w) // walks every page let page = await client.workflows.list({ limit: 50 }) while (page.hasNextPage()) page = await page.getNextPage() try { await client.agents.execute('calculator-agent', { prompt: '6*7' }, { maxRetries: 5 }) } catch (err) { if (err instanceof PlungeAI.FailedDependencyError) console.log(err.connect?.link) else if (err instanceof PlungeAI.APIError) console.log(err.status, err.code, err.requestID) else throw err } const { data, request_id } = await client.whoami().withResponse() ``` - Streams: `break` or `stream.controller.abort()` aborts the request; `stream.tee()`, `stream.toReadableStream()`. `stream: true` on `client.agents.execute` / `client.chat.completions.create` returns the same `Stream`. - Options per request: `maxRetries`, `timeout` (ms), `idempotencyKey`. Client: `new PlungeAI({ maxRetries: 0, timeout: 20_000 })`. - Raw: `.withResponse()` → `{ data, response, request_id }`; `.asResponse()` → the raw `Response`, body unread. - Error classes are exported by name and as statics on `PlungeAI`. - Runtimes: Node.js 20+, Deno, Bun, Cloudflare Workers, Vercel Edge. Browsers refused unless `dangerouslyAllowBrowser: true`. ## Python — `plungeai` ```python from plungeai import PlungeAI, APIError, FailedDependencyError client = PlungeAI() # PlungeAI(api_key="ozk_…", max_retries=0, timeout=20.0) for event in client.workflows.execute_inline_stream(workflow=workflow): if event.event == "token": print(event.json()["delta"], end="") for w in client.workflows.list(kind="bot"): # walks every page print(w) page = client.workflows.list(limit=50) # page.data, page.has_more while page.has_next_page(): page = page.get_next_page() try: client.agents.execute("calculator-agent", prompt="6*7", max_retries=5) except FailedDependencyError as err: print(err.connect["link"]) except APIError as err: print(err.status_code, err.code, err.request_id) response = client.agents.with_raw_response.execute("calculator-agent", prompt="6*7") print(response.status_code, response.headers["x-request-id"]); run = response.parse() ``` - Async: `AsyncPlungeAI()` has every method with the same signature, awaited; streams iterate with `async for event in await …`. - `snake_case` methods; request fields are keyword arguments typed as `TypedDict`s (`plungeai.types`); responses are plain `dict`s. Path parameters come first, positionally: `client.workflows.versions.restore(workflow_id, version)`. - Streams: `event.data` raw string, `event.json()` parsed; leaving the loop closes the response (async: pair an early `break` with `async with await … as stream:` or `await stream.aclose()`). `stream=True` on `agents.execute` / `chat.completions.create` returns a `Stream`. - Per-call options: `extra_headers`, `extra_query`, `timeout` (seconds), `max_retries`, `idempotency_key`. `client.get(path, query=…)`, `client.post(path, body=…)` reach routes outside the generated layer; `executions.continue` is `continue_`. - Python 3.10+, one runtime dependency (`httpx`), ships `py.typed`. ## Go — `github.com/PlungeAI/plungeai-go` ```go client := plungeai.NewClient() // plungeai.NewClient(plungeai.WithAPIKey("ozk_…"), plungeai.WithMaxRetries(0)) stream, err := client.Workflows.ExecuteInlineStream(ctx, plungeai.WorkflowExecuteInlineStreamParams{Workflow: workflow}) if err != nil { return err } defer stream.Close() for stream.Next() { ev := stream.Current() // ev.Event, ev.Data (raw string), ev.JSON(&v) } err = stream.Err() pager := client.Workflows.ListAutoPaging(ctx, plungeai.WorkflowListParams{Kind: "bot"}) // walks every page for pager.Next() { fmt.Println(pager.Current()) } var raw *http.Response _, err = client.Agents.Execute(ctx, "calculator-agent", plungeai.AgentExecuteParams{Prompt: "6*7"}, plungeai.WithResponseInto(&raw)) var apiErr *plungeai.Error if errors.As(err, &apiErr) && apiErr.StatusCode == 424 { fmt.Println(apiErr.Connect.Link) } ``` - Every method: `ctx`, path parameters, a params struct (`plungeai.<Resource><Method>Params`), then options. Nested resources are fields: `client.Workflows.Versions.List(ctx, workflowID)`. - Options (client or per call): `WithAPIKey`, `WithBaseURL`, `WithMaxRetries`, `WithRequestTimeout`, `WithHeader`, `WithQuery` (overrides a same-named params field), `WithIdempotencyKey`, `WithResponseInto`. A deadline on `ctx` bounds the whole call, retries included. - Streaming twins: `Agents.ExecuteStreaming`, `Chat.Completions.CreateStreaming`; list twins: `…AutoPaging`; manual pages: `page.Data`, `page.HasNextPage()`, `page.GetNextPage(ctx)`. - Errors: one type `*plungeai.Error` (`StatusCode`, `Code`, `Message`, `RequestID`, `Connect` on 424); no response → the transport error (`errors.Is(err, context.DeadlineExceeded)` on timeout); no key → `plungeai.ErrMissingAPIKey`. - `client.Get(ctx, path, params, &out)`, `client.Post(ctx, path, body, &out)` reach routes outside the generated layer; the root `POST /v1/execute` is `client.ExecuteCall`. - Go 1.24+, standard library only; module published from `github.com/PlungeAI/plungeai-go`, source `sdk/one-api-go/`. ## Rust — `plungeai` ```rust use futures_util::StreamExt; use plungeai::{types::*, Client, RequestOptions}; let client = Client::builder().max_retries(0).build()?; // or Client::new()? let mut stream = client .workflows() .execute_inline_stream(&WorkflowExecuteInlineStreamParams { workflow: workflow.into(), ..Default::default() }) .await?; while let Some(event) = stream.next().await { let event = event?; // event.event, event.data, event.json::<T>() } let mut items = client.workflows().list(&Default::default()).await?.into_stream(); // walks every page while let Some(w) = items.next().await { println!("{:?}", w?); } match client.with_options(RequestOptions::new().max_retries(5)).agents().execute("calculator-agent", ¶ms).await { Err(plungeai::Error::Api(err)) if err.status == 424 => println!("{:?}", err.connect.map(|c| c.link)), other => { other?; } } let response = client.agents().execute_raw("calculator-agent", ¶ms).await?; println!("{} {:?}", response.status, response.request_id); // response.parse::<AgentExecuteResponse>()? ``` - Resources are methods (`client.agents()`, `client.workflows().versions()`); path parameters first, then a reference to the `Default` params struct. Optional fields `Option<T>`; response enums carry `Unknown`. - `client.with_options(RequestOptions::new()…)` (`header`, `query`, `timeout`, `max_retries`, `idempotency_key`) returns a client carrying those options; every JSON operation has a `…_raw` twin (`status`, `headers`, `request_id`, `parse::<T>()`; paged methods have `list_raw` and every `Page` carries `request_id`); streaming twins `…_stream` (`agents().execute_stream`, `chat().completions().create_stream`); pages `data`, `has_next_page()`, `next_page()`, `into_stream()`. - Errors: `Error::Api(Box<ApiError>)` (`status`, `code`, `message`, `request_id`, `connect`), `Connection`, `Timeout`, `Decode`; predicates `status()`, `is_not_found()`, `is_rate_limit()`. - `client.get(path)`, `client.post(path, &body)` reach routes outside the generated layer. - Rust 1.85+ (edition 2021), `tokio` runtime, TLS through rustls. ## Models plane Every SDK covers the OpenAI-compatible routes (`chat.completions.create`, `embeddings`, `models`); the OpenAI SDK works too with base URL `https://api.plungeai.com/v1` and the same key — see `plungeai-models`. # plungeai-mcp-setup Source: https://www.plungeai.com/docs/skills/plungeai-mcp-setup Connect and operate the PlungeAI MCP server (`https://mcp.plungeai.com/v1`) — endpoint and Bearer ozk_/X-API-Key auth, a generic MCP client config (native remote/HTTP, or the mcp-remote stdio bridge), plungeai_whoami, the structured-outcome envelope and trust fences, the full plungeai_* tool index pointing to the capability skill for each tool, and connection troubleshooting. Use when connecting ANY MCP client to PlungeAI, calling or planning plungeai_* tool calls, polling an execution, resuming a paused run (plungeai_continue), or debugging a 401, a short tool list, or a fenced key. Triggers: "plungeai MCP", "mcp.plungeai.com", "connect an MCP client", "plungeai_whoami", "needs_approval", "execution id". Per-editor install steps live in `plungeai-in-<tool>` — load that instead. REST/codegen is `plungeai-api-setup`; terminal is `plungeai-cli-setup`; CNL authoring is `plungeai-workflows`. [Download zip](https://skills.plungeai.com/plungeai-mcp-setup.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-mcp-setup/SKILL.md) The PlungeAI MCP server turns the whole platform into a control plane for any MCP client: discover and run agents, execute and build CNL workflows, run typed tool calls, bounded autonomous missions, schedules (cron, one-time `@once`, triggers), bots (permissions, workspace files, chat bindings, groups), persistent chat, human-in-the-loop approvals, and long-term memory. Everything it creates uses the same storage as Ocean Studio, so workflows, executions, and conversations cross-appear in every app and door. ## Prerequisites Self-service `ozk_` key: **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`) → create → copy it once. Ask the account owner only for a shared/team key minted under someone else's account. ## Connect Endpoint `https://mcp.plungeai.com/v1` (Streamable HTTP), auth header `Authorization: Bearer ozk_YOUR_KEY` (or `X-API-Key: ozk_YOUR_KEY`). `POST https://api.plungeai.com/v1/mcp` is the identical server on the API host, for a client that can only reach one host. One-click install page: `https://mcp.plungeai.com/install` (machine form: `/install.json`) — type the key into the box **before** clicking a button, so the deeplink is rewritten with the Authorization header baked in; clicked without a key the server installs auth-less and you add the header by hand afterward. **Generic client config** (any MCP client not covered by a `plungeai-in-<tool>` skill): 1. **Native remote/HTTP support (preferred):** URL `https://mcp.plungeai.com/v1`, header `Authorization: Bearer ozk_YOUR_KEY`. 2. **stdio-only client:** bridge with [mcp-remote](https://github.com/geelen/mcp-remote) (Node 18+): ```json { "mcpServers": { "plungeai": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.plungeai.com/v1", "--header", "Authorization: Bearer ozk_YOUR_KEY"] } } } ``` Clients that mangle spaces inside `args` (mcp-remote's README names Cursor, Codex CLI, and Claude Desktop on Windows): pass `"--header", "Authorization:${AUTH_HEADER}"` plus `"env": { "AUTH_HEADER": "Bearer ozk_YOUR_KEY" }` in the same entry. The bridge is an `npx` process — a cold start can exceed a client's default startup timeout (Codex default: 10s); raise it where the client has one. 3. Neither → skip MCP and use the One API instead (`plungeai-api-setup`) — every capability is reachable over plain HTTPS. **Key handling.** User-level config, never committed: the literal key lives only in the tool's home config or its secret store, never in a repo file. A project-level config may only reference an env var. Before the first commit in any repo you just configured: ```bash git grep -n "ozk_" # tracked files grep -rn "ozk_" . --exclude-dir=.git --exclude-dir=node_modules # untracked too ``` Both must print nothing. ## Verify ``` plungeai_whoami {user_request: "verify my plungeai connection"} ``` Expect: user id, auth type + tier, key label, rate-limit window (used/minute, used/day), and the server identity (`mcp.plungeai.com`). A 401 means the key is missing/invalid — fix the header, don't retry blind. There is no permissions field on the card — fencing shows up as which tools `tools/list` returns, not on `whoami`. One real call proves the catalog too: `plungeai_list_agents {search: "web search"}` → live results. ## The five rules that prevent most failures 1. **Discovery first, ids never from memory.** The agent catalog is live and active-only. `plungeai_list_agents {search: "<capability in plain words>"}` is a hybrid semantic+keyword search — take agent ids verbatim from its results. An unknown or non-active id is REFUSED at execution (nothing runs); re-discover, never guess or reuse an id from an earlier session. 2. **Relay results verbatim.** Every successful tool output is final, platform-rendered, user-ready markdown. Present it in full — do not re-write, shorten, or summarize unless asked. The parts addressed to you are the guidance tails: execution-id footers, poll hints, and ⏸ continuation blocks — act on those. 3. **Structured outcomes, never blind retries.** Execution tools answer with a status envelope (`ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error`), never a raw error. Follow the `remediation`; never retry the identical call. 4. **⏸ approval fences are the user's, not yours.** A run pausing with `⏸ AWAITING USER APPROVAL: <action> (cost)` or `⏸ AWAITING USER: <question>` must be relayed verbatim. Only after the user decides do you call `plungeai_continue` (`approve: true` ONLY for an explicit yes; `message: "<their words>"` for an answer, denial, or change of course). NEVER approve on your own. 5. **Pass `user_request` on every call** — the user's original ask, verbatim, before you translated it into arguments. Every tool accepts it; the platform uses it for routing and support diagnostics. **Terminology trap:** users call their saved workflows "agents" too. An unqualified "show me my agents" means their SAVED WORKFLOWS → `plungeai_list_workflows`, not the registry (`plungeai_list_agents`). ## Sync vs async, and continuation - **Sync (default on most tools):** streams progress, returns the finished result. Typical workflows finish in seconds (parallel RPC fan-out). - **Async (`mode: "async"`):** returns an `execution_id` immediately; the run continues server-side. Use it for anything that could exceed ~3 minutes (Claude Desktop hard-caps a tool call at ~4 minutes). Poll `plungeai_get_workflow_status`, fetch with `plungeai_get_result`. A per-run Durable Object holds it in resumable ~10-minute legs, so it survives a client disconnect (no live progress feed); a run silent for 16 minutes is marked failed. `plungeai_run_mission`/`plungeai_learn` default to async; `plungeai_execute_agent` has no `mode` — always sync. - **⏸ Continuation:** the pause appears as a tail on the result, on `plungeai_get_result`, and in `plungeai_get_workflow_status.structuredContent.continuation`. Relay it verbatim → ask the user → `plungeai_continue {execution_id, approve: true}` (yes) or `{execution_id, message: "<their words>"}` (no/change). The execution id doubles as the conversation session id. ## Tool map The catalog is served live by `tools/list` (22 tools, server 2.7.0 — `tools/list` is authoritative; a fenced key sees fewer). Every tool's parameters and every `action` value: [`references/tool-actions.md`](/skills/plungeai-mcp-setup/references/tool-actions); the detail of each lives in the capability skill named below. | Tool | One-line purpose | Capability skill | |---|---|---| | `plungeai_whoami` | The authenticated identity, tier, key label, rate window | this skill | | `plungeai_list_agents` | Search the live agent registry (semantic); fetch one full card; `kind` also reaches personas, experts, skills, models, workflows, connectors | `plungeai-discovery` | | `plungeai_get_tool_contract` | Exact invocation contract for one agent: schema, operations, credential status | `plungeai-discovery` | | `plungeai_execute_agent` | Run one prompt-driven agent once | `plungeai-agents` | | `plungeai_execute_tool` | Run one structured tool-agent with typed `{agent_id, operation, params}` | `plungeai-tools-connectors` | | `plungeai_get_result` | Fetch a run's output by execution id; `task_id` reads a single step | `plungeai-results-traces` | | `plungeai_execute_workflow` | Run a saved workflow or ad-hoc CNL YAML; sync streams, async polls | `plungeai-workflows` | | `plungeai_get_workflow_status` | Poll a run: status, error, duration, `continuation` when paused | `plungeai-workflows` | | `plungeai_list_workflows` | The user's saved workflows, agents, bots and campaigns (`kind`) | `plungeai-workflows` | | `plungeai_workflow` | Workflow CRUD + versions, `a2a_door`, and the bot actions: permissions, workspace files, inbox, chat bindings, bot groups | `plungeai-workflows` | | `plungeai_build_workflow` | Generate a workflow from a goal, or refine one | `plungeai-workflows` | | `plungeai_executions` | Execution history: list/get/output/conversation/files/delete, and cancel or steer a live run | `plungeai-results-traces` | | `plungeai_chat` | Persistent chat with the platform assistant | `plungeai-results-traces` | | `plungeai_followup` | Ask a follow-up on a run (queued while it still runs) | `plungeai-results-traces` | | `plungeai_continue` | Resume a paused run: answer a question or deliver approval | this skill (above) | | `plungeai_run_mission` | Bounded autonomous agent mission (tool fence, iteration cap) | `plungeai-missions` | | `plungeai_learn` | Your private skills: learn (distill a URL/text/session), list, forget | `plungeai-memory` | | `plungeai_schedule` | Cron and one-time (`@once` + `run_at`) jobs: stats/list/get/create/update/pause/resume/delete/run_now/runs/wake, plus webhook, email and hook triggers | `plungeai-scheduling` | | `plungeai_memory` | Long-term memory: recall/remember/search_runs/get_run | `plungeai-memory` | | `plungeai_templates` | Workflow templates: list/get/use | `plungeai-workflows` | | `plungeai_companies` | Companies (campuses compiled from saved campus/building/office documents), as One API `/v1/companies`: list/apply/layout/snapshot/events/ingest/slice, items, approvals/decide, mode, start/stop, delete | this skill ([`references/tool-actions.md`](/skills/plungeai-mcp-setup/references/tool-actions)) | | `plungeai_connections` | Connected accounts, yours or a customer's (`on_behalf_of`), as One API `/v1/connections`: list/add/remove/link | `plungeai-tools-connectors` | **Beyond tools**: 8 MCP resources (registry or the caller's own data, ownership-checked) — `plungeai://agents/list`, `/agents/categories`, `/agents/{category}`, `/personas/list`, `/workflows/list`, `/workflows/{id}`, `/executions/{id}`, `/docs/workflow-authoring` — and one prompt, `/plungeai`, that primes a client with the operating instructions. ## Auth model, outcome envelope, trust fences **An API key IS a user identity** — the key resolves to a user id, and every read/write on every tool is scoped to what that user owns, the same view they have in Studio. A foreign id answers "not found," not "forbidden." Execution tools never return a raw error: the answer is text plus `structuredContent` with `status: ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error`, `summary`, and typed `remediation.actions` (`connect_provider`, `provide_api_key`, `provide_field`, `approve_via`/`respond_via`, `retry_with`, `use_alternative`). `isError` is true ONLY for terminal `error` outcomes — every other status is guidance, not a crash. Trust fences (surface, never retry, never bypass): the **active-only fence** refuses an unknown/inactive agent id before dispatch — re-discover, never retry the same id. The **approval fence** (`needs_approval` / ⏸) is a deliberate human boundary — only the user decides. **Connection fences** (`needs_connection`/`needs_api_key`) need the user's browser (Studio → Connectors) — relay instructions, wait, then retry the identical call once they confirm (the one case where repeating the same call is correct). Full protocol-error table, rate limits (free 30/min·1k/day, pro 100/min·10k/day, enterprise 300/min·100k/day), per-key fences (`allowed_ips`, `allowed_tools`), and the failure-classification table: [`references/identity-and-errors.md`](/skills/plungeai-mcp-setup/references/identity-and-errors). ## Troubleshooting | Symptom | Cause → fix | |---|---| | 401 (`No valid authentication provided…`; `WWW-Authenticate: Bearer error="invalid_token"` = a key arrived and was rejected) | Key typo'd, pasted with whitespace, or header never sent → fix the header in the client's config | | Connected, no `plungeai_*` tools — or fewer than expected | Client not reloaded after the config edit, or the key is fenced to a subset (`allowed_tools`): policy, not a bug | | 405 or an immediate connect error | Client is SSE-only or stdio-only (`GET /mcp` answers 405 at once) → use the mcp-remote bridge | | Bridge "never appears" / times out | npx cold start exceeded the client's startup timeout, or Node.js is missing | | 401 only through the bridge | Client mangled the spaces in `--header` → use `Authorization:${AUTH_HEADER}` + `env` (above) | | Works for one teammate, not another | Separate keys, separate fences — per-machine config | | 403 `refused` / 409 `approval_required` | Trust fence, not a connection problem — relay to the user | Per-tool reload steps and config paths: `plungeai-in-<tool>` (twelve builders). ## References - [`references/tool-actions.md`](/skills/plungeai-mcp-setup/references/tool-actions) — the 22 tools' parameters and every `action` value. - Full guide: https://mcp.plungeai.com/docs#1-quickstart — every tool's parameters and a `tools/call` example at https://mcp.plungeai.com/docs#14-tool-reference. - [`references/identity-and-errors.md`](/skills/plungeai-mcp-setup/references/identity-and-errors) — `plungeai_whoami`, the auth model, the two error planes (protocol vs tool-outcome), rate limits, per-key fences, failure classification. ## Related skills - `plungeai-in-<tool>` — exact per-editor connect steps and quirks. - `plungeai-api-setup` — the same platform over REST instead of MCP. - `plungeai-cli-setup` — operate PlungeAI from a terminal. - `plungeai-sdk-setup` — the official TypeScript, Python, Go and Rust SDKs for code you write. - `plungeai-workflows` — CNL YAML authoring deep-dive. - The capability skills named in the Tool map — per-tool detail. ## Reference pages <CardGroup cols={2}> <Card title="Identity & errors — whoami, the auth model, outcome envelope, trust fences" icon="file-text" href="/skills/plungeai-mcp-setup/references/identity-and-errors"> --- </Card> <Card title="The 22 tools — parameters and every action" icon="file-text" href="/skills/plungeai-mcp-setup/references/tool-actions"> Names, parameters and action values below are the zod schemas the server validates against; tools/list is still authoritative for the key in hand (a fenced… </Card> </CardGroup> # Identity & errors — whoami, the auth model, outcome envelope, trust fences Source: https://www.plungeai.com/docs/skills/plungeai-mcp-setup/references/identity-and-errors <!-- sources-of-truth: orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/README.md, orchestration/mcp-gateway/extras.ts, orchestration/mcp-gateway/auth-middleware.ts, orchestration/mcp-gateway/index.ts, orchestration/mcp-gateway/tool-outcome.ts, orchestration/mcp-gateway/server.ts | last-synced: 2026-10-05 (MCP 2.6.0; re-verified: 20-tool catalog against server.ts TOOL_DEFS, auth order and unsupported_bearer_token against auth-middleware.ts, -32602 Tool not permitted and the 1 MiB body cap against index.ts, rate tiers against the REST errors guide, and live 2026-10-05: a rejected key and a missing key both answer 401 with the same message, told apart by the WWW-Authenticate header) --> --- ## plungeai_whoami **Purpose:** show the authenticated identity — the ground truth for "who am I acting as" and "how much budget is left". Read-only, no arguments beyond `user_request`. Use it to verify a new connection, and whenever ownership ("Execution not found") or rate-limit errors appear. **Example:** `{"user_request": "verify my plungeai connection"}` **Returns** ``` # Who am I - **User:** `<uuid>` - **Auth:** api-key · tier **pro** - **Key:** <label> (`<key id>`) - **Rate window:** N/min used · M/day used - **Server:** plungeai.com (mcp.plungeai.com) ``` **Failures & fixes:** "Authentication required." → the key never reached the server; fix the header (`Authorization: Bearer ozk_YOUR_KEY` or `X-API-Key: ozk_YOUR_KEY`). The rate window line is best-effort and may be absent — that alone is not an error. --- ## The auth model **An API key IS a user identity.** The key resolves to a user id, and every read/write on every tool is scoped to what that user owns — the same view that user has in Studio, never the whole system. Every id-based operation (executions, workflows, jobs, conversations, memory) is ownership-checked before any storage is read; a foreign id answers "not found", not "forbidden" — so a plausible-looking id that returns "Execution not found" usually belongs to someone else or is mistyped. Auth order: an internal service path an API key can never reach → `X-API-Key` → `Authorization: Bearer` (personal API keys, e.g. `ozk_YOUR_KEY`; any other bearer value is rejected — logged as `unsupported_bearer_token` — only `ozk_` keys authenticate, there is no OAuth token path today). Unauthenticated requests get **401** with a `WWW-Authenticate: Bearer` challenge (`error="invalid_token"` when credentials were supplied but invalid). Keys are self-service — mint your own at **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`); an account owner can additionally fence (`allowed_ips`/`allowed_tools`) or revoke any key on the account, including a shared team key. **Per-key fences** (both optional, set at mint): - `allowed_ips` — the key only authenticates from listed addresses/CIDRs; anywhere else is a 401 (and counts against failed-auth metering). - `allowed_tools` — other tools are HIDDEN from `tools/list`, and calling one returns `-32602 Tool not permitted for this key`. If a documented tool is missing from `tools/list`, the key is fenced — re-mint or edit the key's fence in the Dashboard; do not keep calling it. **Rate limits** are per tier: free 30/min · 1k/day, pro 100/min · 10k/day, enterprise 300/min · 100k/day (tool calls only). Over-limit → HTTP 429 with a `Retry-After` header — wait that long, then resume; never tight-loop. **Activity trail:** every request is recorded to a per-user daily log (arguments in full, answers as receipts). A user reads their own with their own key: `https://mcp.plungeai.com/log?date=YYYY-MM-DD` (Bearer header, or `&key=ozk_YOUR_KEY` in a browser). This is also why `user_request` matters: it is the one place the user's actual sentence survives into diagnostics. --- ## Two error planes **1. Protocol errors (JSON-RPC, transport-level)** — the request itself was rejected; no tool ran: | Signal | Meaning | Your move | |---|---|---| | 401 + `WWW-Authenticate` | Missing/invalid/revoked key, or IP outside the key's fence | Fix the credential; don't retry blind. Repeated failures meter the IP (429 with Retry-After). | | 429 + `Retry-After` | Rate limit (per-tier), or failed-auth metering | Wait the stated seconds, resume, consider batching. | | `-32602` "Tool not found" / "Tool not permitted for this key" | Typo'd tool name, or a key fence | Re-check against `tools/list`; if fenced, edit the key's fence in the Dashboard. | | `-32700` Parse error / 413 body too large (1 MiB) | Malformed or oversized request | Fix the payload; huge YAML belongs in a saved workflow, not inline. | | 405 / 403 invalid Host / `-32603` internal | Transport misuse or server fault | Use POST to the canonical endpoint; internal errors: retry once, then report. | One deliberate crossover: arguments that fail a tool's own schema (e.g. `limit: 200`) are pre-screened BEFORE the tool runs and still answer in the envelope — `needs_input`, "`<tool>` was not called — its arguments do not match the tool's schema", with `provide_field` actions per bad field. Nothing ran; fix the named fields against the tool's `inputSchema` from `tools/list`. **2. Tool outcomes (the envelope)** — the tool ran (or deliberately refused) and answered with structured guidance. Execution tools NEVER return a raw error: the answer is text plus `structuredContent` with ``` status: ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error summary, agent_id?, operation?, result?, execution_id?, missing?[], warnings?[], contract?, remediation: { message, actions: [...] } ``` Typed `remediation.actions` tell you the exact next move: `connect_provider` (user opens the named URL and connects — needs their browser), `provide_api_key` (user saves a key in a PlungeAI app), `provide_field` (fix that field, retry the same call), `approve_via` / `respond_via` (the ⏸ loop → `plungeai_continue`), `retry_with` (retry with the stated changes, e.g. `{mode: "async"}`), `use_alternative` (a real, callable substitute agent id). `isError` is true ONLY for terminal `error` outcomes. Every other non-ok status is guidance the model acts on — the response even ends with "This is structured guidance, not a crash — do not retry the identical call." Believe it. --- ## The trust fences (surface, never retry, never bypass) **The active-only fence.** Only `status:active` agents are listed and callable over MCP. `plungeai_execute_agent`, `plungeai_execute_tool`, ad-hoc YAML in `plungeai_execute_workflow` (including agents nested in `parallel`/`sequential` blocks), and `plungeai_schedule` create all refuse an unknown or inactive id BEFORE dispatch, writing no execution row. Every refusal's prose points you at `plungeai_list_agents`; `plungeai_execute_agent` and `plungeai_execute_tool` additionally attach live `use_alternative` suggestions to their `unavailable` envelope — `plungeai_execute_workflow`'s fence refusal carries no remediation actions, and `plungeai_schedule` create answers with plain error text, not an envelope. A refusal is a policy answer, not a transient failure: **always re-discover** with `plungeai_list_agents` (semantic `search`), take a fresh id from the results, and never "retry" the refused id or guess a variant spelling. **The approval fence.** Gated operations and real-world side effects pause as `needs_approval` / `⏸ AWAITING USER APPROVAL` (a paused run also shows it in `plungeai_get_workflow_status` → `continuation`). This is a deliberate trust boundary between the platform and the human: relay it verbatim, let the USER decide, and only then `plungeai_continue {approve: true}` (yes) or `{message: "<their words>"}` (no / change). Approving on your own, retrying around the pause, or resubmitting the original call are all violations — the platform holds the pending action server-side until the user answers. **Connection fences.** `needs_connection` / `needs_api_key` mean the USER must act in their browser (connect an account or save a key in a PlungeAI app, e.g. Studio → Connectors, per the remediation's URL). Relay the exact instructions, wait for their confirmation, then retry the IDENTICAL call — this is the one case where the same call is expected to be repeated, and only after the user acted. **Ownership answers.** "not found / not one of your executions" on a plausible id is an ownership verdict, not a storage glitch — re-list (`plungeai_executions`, `plungeai_list_workflows`, `plungeai_schedule {action: "list"}`) and use an id the account actually owns. --- ## Failure classification you can rely on Whatever the pre-flight didn't predict, the engine's raw failure is classified into the same envelope — a named missing API key becomes `needs_api_key`, an upstream 401 becomes `needs_connection` naming the provider to connect, insufficient OAuth scopes become "reconnect to grant additional scopes", missing/invalid inputs become `needs_input` WITH the agent's contract attached, an undeployed binding becomes `unavailable` with alternatives, and a timeout becomes `error` with `retry_with {mode: "async"}`. A failed run always carries a reason — `plungeai_get_result` on a failed id answers "This run failed: <reason>" rather than a dead end. So: read the status, follow the remediation, and reserve blind one-time retries for the one class labeled transient ("This is a transient platform failure ... Retry once in a few seconds", stamped with a request id to report if it persists). # The 22 tools — parameters and every action Source: https://www.plungeai.com/docs/skills/plungeai-mcp-setup/references/tool-actions <!-- sources-of-truth: orchestration/mcp-gateway/server.ts, orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/tools-bots.ts, orchestration/mcp-gateway/tools-triggers.ts, orchestration/mcp-gateway/tools-companies.ts, orchestration/mcp-gateway/tools-connections.ts, orchestration/mcp-gateway/extras.ts, orchestration/mcp-gateway/tool-exec.ts | last-synced: 2026-10-14 (MCP 2.7.0: 22 tools; action enums checked against the zod schemas) --> Names, parameters and `action` values below are the zod schemas the server validates against; `tools/list` is still authoritative for the key in hand (a fenced key sees fewer tools). The One API twin of each family is in `plungeai-api-setup`. ```text PlungeAI MCP 2.7.0. Server https://mcp.plungeai.com/v1 (Streamable HTTP), Authorization: Bearer ozk_YOUR_KEY. 22 tools; tools/list is authoritative (a fenced key sees fewer). Pass user_request (the user's own words, verbatim) on every call. Output is final markdown: relay it verbatim. format:"json" returns machine-readable output. FIND: plungeai_list_agents {search, category, agent_id, kind: agents|personas|experts|skills|models|workflows|connectors} = the live registry; ids come only from here. plungeai_get_tool_contract {agent_id} = schema, operations, credential status. plungeai_whoami = identity, tier, key label, rate window. RUN: plungeai_execute_agent {agent, prompt} (always sync). plungeai_execute_tool {agent_id, operation, params, mode}. plungeai_run_mission {goal, allowed_tools, max_iterations} (async by default). plungeai_execute_workflow {workflow_id | workflow_yaml, input, inputs, mode: sync|async}. RUNS: plungeai_get_workflow_status {execution_id} polls (and carries continuation). plungeai_get_result {execution_id, task_id?}. plungeai_followup {execution_id, prompt}. plungeai_continue {execution_id, approve | message}. plungeai_executions {action: list|get|output|conversation|delete|cancel|steer|files; message for steer}. WORKFLOWS: plungeai_list_workflows {kind: workflow|agent|bot|campaign, folder, search}. plungeai_build_workflow {goal | workflow_id + instruction}. plungeai_templates {action: list|get|use}. plungeai_workflow {action: create|get|update|delete|list_versions|get_version|save_version|restore_version|a2a_door|permissions_get|permissions_set|files|file|inbox|bindings|bind|unbind|groups|group_set|group_delete|group_thread; kind}. SCHEDULES: plungeai_schedule {action: stats|list|get|create|update|pause|resume|delete|run_now|runs|wake|webhook_mint|webhook_list|webhook_delete|email_mint|email_list|email_delete|hooks_mint|hooks_list|hooks_delete}; create takes job_type agent|query|workflow|heartbeat, target, schedule (cron, or "@once" with run_at = ISO-8601 with a UTC offset). COMPANIES: plungeai_companies {action: list|apply|layout|snapshot|events|ingest|slice|item_create|item_pause|item_resume|item_retry|item_cancel|item_steer|approvals|decide|mode|start|stop|delete; campus on every action but list}. CONNECTIONS: plungeai_connections {action: list|add|remove|link; provider, api_key, api_secret, return_url, on_behalf_of}. MEMORY: plungeai_memory {action: recall|remember|search_runs|get_run}. plungeai_learn {action: learn|list|forget} (learn defaults to async). plungeai_chat {action: send|list_sessions|history|new}. OUTCOMES: status ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error, with remediation.actions. A paused run ends with a pause block: relay it verbatim, call plungeai_continue only after the user decides (approve:true for an explicit yes, message for anything else). Never approve for the user; never retry the identical call blind. ``` ## Notes that bite - `plungeai_workflow` bot actions need `workflow_id` of a saved bot; `file` returns at most 5 MB inline (base64) — larger files come from the One API `GET /v1/workflows/{id}/workspace-files/download?path=`. - `plungeai_workflow` `permissions_set` replaces each field you send; `locks` need a team admin over the workflow. - `plungeai_workflow` `bind`/`unbind` take `channel` (telegram, whatsapp, discord, teams) and `sender_id`; `group_set` takes `group` (a name) and `members` (2–6 of your bot ids). - `plungeai_executions` `steer` takes `message` and only reaches a running execution; `files` lists what the run wrote. - `plungeai_schedule` trigger actions take the schedule as `job_id`; a minted secret is shown once; `wake` takes `job_id` and `note`. - The MCP `plungeai_schedule` `job_type` accepts `agent` and `query` as well as `workflow` and `heartbeat`; the One API route `POST /v1/schedules` takes only `workflow` and `heartbeat`. - `plungeai_companies` `decide` takes `approval_id` and `decision` (`approve`|`reject`) — only after the user decided; `delete` of a running campus is refused with `conflict` (`stop` first). - `plungeai_connections` never returns key material; `link` mints a signed single-use Studio link (15 minutes) for whoever owns the account. # ocean-cli Source: https://www.plungeai.com/docs/skills/ocean-cli Drive PlungeAI Ocean from a terminal with the ocean-cli command (package @plungeai/ocean): web search, extract and deep research, registry agents and tool agents, saved workflows and bots, executions, missions, schedules and their triggers, memory, templates, the registry, and the Studio account surfaces. Use when an agent has shell access and the task touches PlungeAI — prefer this over the MCP server in Claude Code, Cursor, Codex, Copilot, OpenCode or OceanCode; fall back to `https://mcp.plungeai.com/v1` when there is no shell. [Download zip](https://skills.plungeai.com/ocean-cli.zip) · [View raw SKILL.md](https://skills.plungeai.com/ocean-cli/SKILL.md) `ocean-cli` is a non-interactive remote client: every command talks to the deployed PlungeAI hosts (`https://api.plungeai.com`, `https://mcp.plungeai.com/v1`, `https://studio.plungeai.com`) and nothing runs locally. Every verb takes `--json`, long operations take `--no-wait` plus a `status`/`poll` verb, exit codes are stable (0 ok · 2 usage · 3 auth · 4 refused/not ready/rate limited · 5 timeout), and status lines go to stderr. ## 1. Make sure it is installed and logged in ```bash which ocean-cli || npm install -g @plungeai/ocean # Node ≥ 20; once published (next paragraph); or: npx @plungeai/ocean --version ocean-cli --version # ocean-cli, version 2.x ocean-cli auth --json # {"authenticated": true, …} — else the user runs: ocean-cli login (browser sign-in) ocean-cli doctor # live check of every plane; exit 4 lists what is missing ``` `@plungeai/ocean` is not on npm yet: until the `ocean-v2.1.0` tag is published, run it from the repository (`npm run build:ocean-cli` + `node apps/ocean-cli/dist/index.js`). A missing credential exits 3 with the exact `login` command to run. `ocean-cli login` opens the browser and stores the `ozk_` key; over SSH or in a headless shell the user runs `ocean-cli login --no-browser` (hidden prompt) — never paste a key on the command line. Studio verbs (`keys`, `balance`, `usage`, `nodes`, `campaigns`, `connections`, `secrets`, `documents`, `teams`, `projects`, `executions export`) need `ocean-cli login --session`. Environment variables `OCEAN_API_KEY`, `OCEAN_SESSION` override the stored credentials for one process and are never written to disk. ## 2. Discover before you guess The flags below change between versions. Always confirm with `--help` before composing a call you have not run in this session; the complete tree is in [`references/verbs.md`](/skills/ocean-cli/references/verbs). ```bash ocean-cli --help # every noun ocean-cli research --help # verbs of a noun ocean-cli research run --help # flags of a verb, with examples ocean-cli discovery search "web search" --kind agents --limit 5 --json # discover agents, tools, skills, personas live ocean-cli agents list --limit 20 --json · ocean-cli tools get <agent_id> --json # an agent's card / a tool agent's contract ``` ## 3. Task → command | The user wants | Run | |---|---| | a web answer with sources | `ocean-cli search "<objective>" --max-results 5 --json` (keyword mode: `-q "<kw>" -q "<kw>"`); `ocean-cli answer "<question>" --effort low --json` for a cited answer | | a page as clean markdown | `ocean-cli extract <url> [--objective "<focus>"] --json` | | deep research (minutes, paid) | `ocean-cli research run "<question>" -p lite-fast --no-wait --json` → `ocean-cli research poll <run_id> --timeout 900 -o report --json` (writes `report.json`; add `--text` on `run` for a markdown report) | | find entities / enrich rows (paid) | `ocean-cli findall run "<objective>" -g base -n 25 --json`; `ocean-cli enrich run --source rows.csv --intent "<what to add>" --target out.csv` | | run one platform agent | `ocean-cli agents run <agent_id> "<prompt>" --json` (`--stream` for live tokens); read later with `ocean-cli agents result <workflow_id> <task_id> --json` | | call a tool agent with typed params | `ocean-cli tools get <agent_id> --json` for the contract, then `ocean-cli tools call <agent_id> <operation> --params '{"…"}' --json` | | design a workflow from a goal | `ocean-cli workflows build --goal "<what it should do>" --json` → save the YAML to a file → `ocean-cli workflows validate <file>` → `ocean-cli workflows create "<name>" <file> --json` | | change a saved workflow | `ocean-cli workflows build --workflow-id <id> --instruction "<change>" --json` → validate → `ocean-cli workflows update <id> -f <file>`; history: `workflows versions <id>` / `workflows save-version <id> --label "<why>"` / `workflows restore <id> <version_id> --yes` | | run a workflow | `ocean-cli workflows run <id-or-file.yaml> -i "<input>" --json` (waits); `--no-wait` → `ocean-cli executions poll <execution_id> --json` | | see runs and results | `ocean-cli executions list --limit 10 --json`, `ocean-cli executions output <id>`, `ocean-cli executions conversation <id> --json`, `ocean-cli executions followup <id> "<question>"`, `ocean-cli executions files <id>` | | steer or answer a live run | `ocean-cli executions steer <id> "<text>"` (queued for the running loop; a finished run exits 4); `ocean-cli executions continue <id> --message "<answer>"` or `--approve` for a paused run | | a bounded autonomous run with memory | `ocean-cli missions run "<goal>" --max-iterations 5 --no-wait --json` → `ocean-cli missions poll <id> -o mission.json --json` (`--sync` answers on one connection) | | schedule something | `ocean-cli schedules create "<name>" --type agent --target <agent_id> --prompt "<prompt>" --cron "0 9 * * 1-5" --deliver inapp`; once: `--type workflow --target <id> --cron @once --at <ISO time with UTC offset>`; `schedules list`, `pause`, `resume`, `runs`, `wake <id> --note "<why>"`, `delete <id> --yes` | | start a schedule from outside | `ocean-cli schedules triggers webhook mint <job_id> --scheme github`, `triggers email mint <job_id>`, `triggers hooks mint <job_id> --url <https-url>` (outbound signed run events); each has `list` and `delete … --yes`; a minted secret is shown once | | remember / recall | `ocean-cli memory recall`, `ocean-cli memory remember --add "<fact>"`, `ocean-cli memory search-runs "<query>" --limit 5 --json` | | templates, learned skills, chat | `ocean-cli templates list`, `templates use <id> --name "<name>"`; `ocean-cli learn add <url> --name <id>`; `ocean-cli chat send "<message>" [--conversation-id <id>]` | | account, keys, spend | `ocean-cli whoami`, `ocean-cli keys list --json`, `ocean-cli balance --json`, `ocean-cli usage --by model --range 30d` | | local nodes, connections, secrets, documents, teams, projects | `ocean-cli nodes list --json`, `connections list`, `secrets list --team-id <id>`, `documents list`, `teams list`, `projects list` (Studio session) | | export a result as a file | `ocean-cli executions export <id> -f docx -o result.docx` (`pptx`, `xlsx`, `pdf`, `finmodel`, `package`, `google-docs`) | | install the PlungeAI skills | `ocean-cli skills list`, `ocean-cli skills install --project --skill <name>` | ## 4. Bots (saved workflows of kind `bot`) A bot is a saved workflow with `kind: bot`; its policy, files, inbox and chats are verbs on `workflows`, and named sets of bots are `bot-groups`. | The user wants | Run | |---|---| | list or create bots | `ocean-cli workflows list --kind bot --json`; `ocean-cli workflows create "<name>" <file> --kind bot` | | see or change what a bot may do | `ocean-cli workflows permissions get <bot_id> --json`; `ocean-cli workflows permissions set <bot_id> --class pay=ask --class send=allow --budget-run 0.5 --budget-month 20` (classes `send`, `write`, `pay`, `delete`; presets `allow`, `ask`, `deny`, `hand_off`; `--lock`/`--unlock` are team admins only; add `--dry-run` first) | | a bot's workspace files and inbox | `ocean-cli workflows files <bot_id>`, `ocean-cli workflows files <bot_id> --download <path> --out <file>`, `ocean-cli workflows inbox <bot_id>` | | chats bound to a bot | `ocean-cli workflows bindings list <bot_id>`, `… bind <bot_id> --channel telegram --sender <sender_id>`, `… unbind <bot_id> --channel telegram --sender <sender_id> --yes` (the chat is paired in Studio first) | | groups of bots | `ocean-cli bot-groups list`, `ocean-cli bot-groups create --name <name> --members <bot_id>,<bot_id>` (2 to 6 members), `update <id> --name … --members …`, `thread <id>`, `delete <id> --yes` | ## 5. Rules that keep the user safe - Never put a key, session or secret value on a command line; `login --no-browser`, `connections add-key` and `secrets set` read from a hidden prompt, from `--key -`/`--value -` on stdin, or from a file. - Destructive verbs (delete, revoke, restore, remove, forget, unbind) require `--yes` and exit 2 without it — ask the user to confirm before adding `--yes`. Use `--dry-run` to show a paid or creating call before it runs. - `monitor create` is refused by the unattended fence (exit 4 `approval_required`): report it, do not retry; the user approves from Studio. Priced verbs take `--max-cost <usd>`. - The free tier allows 30 requests per minute (exit 4 with a `Rate limit` message): wait the seconds it names, then retry once. - Parse `--json` output; show the human output to the user. `research`, `findall`, `missions` and `enrich` write result files (`-o` sets the path; existing files are not overwritten without `--force`). ## 6. When to use the MCP server instead No shell (claude.ai, Claude Desktop, ChatGPT), or an approval-gated operation the user wants to confirm in-chat: connect `https://mcp.plungeai.com/v1` with `Authorization: Bearer ozk_YOUR_KEY` — see the `plungeai-mcp-setup` skill. Everything the CLI does is also a One API route (`https://api.plungeai.com/v1/openapi.json`) for your own code. ## Reference pages <CardGroup cols={2}> <Card title="ocean-cli — every command and flag (generated)" icon="file-text" href="/skills/ocean-cli/references/verbs"> ocean-cli — every command and flag (generated) </Card> </CardGroup> # ocean-cli — every command and flag (generated) Source: https://www.plungeai.com/docs/skills/ocean-cli/references/verbs <!-- sources-of-truth: apps/ocean-cli/src/index.ts apps/ocean-cli/src/commands/ | generated by apps/ocean-skills/scripts/build-ocean-cli-reference.mjs — do not edit --> ## ocean-cli ``` Usage: ocean-cli [options] [command] Ocean CLI — search, research, run agents and workflows, schedule, and manage your PlungeAI Ocean account from the terminal. Options: -V, --version output the version number -q, --quiet Suppress status lines on stderr -h, --help display help for command Commands: login [options] Sign in through the browser and store your account key (ozk_…), which opens every plane, or with --session the optional Studio session. logout [options] Remove stored credentials. auth [options] Check authentication status. whoami [options] Show the identity, tier and rate window behind the API key. spec [options] The One API OpenAPI document (GET /v1/openapi.json); no key needed. doctor [options] Live connectivity and credential checks (exit 4 when any fails). config [options] [key] [value] View or set CLI configuration. search [options] [objective] Search the web with PlungeAI's search agents (Parallel by default). extract [options] <urls...> Extract content from URLs as clean markdown. answer [options] [question] A reasoning-enhanced, cited answer from the web (Parallel Answer). research Deep research for open-ended questions (Parallel Task API). findall FindAll: discover entities from the web using natural language. monitor Monitor: continuously track the web for changes. enrich Data enrichment: add web-researched columns to a list of rows. agents Registry agents: list, inspect and run them (agents plane). tools Structured tool agents: contracts and typed calls (tools plane). workflows Saved workflows (CNL): list, create, run, versions. bot-groups Bot groups: named sets of 2 to 6 of your saved bots (message_bot to=group:<name>). call [options] <file> Agent Call: POST a program — a building block (agent: brave-agent + its fields), a workflow, or a saved agent (agent: <id>[@n] + input) — to /v1/execute. "-" reads it from stdin. companies Companies in YAML: apply a campus, run its clock, put items into buildings, decide at the door. channels Paired chat channels (Telegram, WhatsApp, Discord, Teams). executions Runs: list, status, poll, stream, output, continue, steer, files, export. missions Bounded autonomous agent runs with memory (type: harness). schedules Scheduled jobs (cron): agents, queries, workflows, heartbeats. memory Long-term platform memory: recall, remember, forget, past runs. learn Distil a URL or text into a private skill. templates Workflow templates. chat One-shot platform chat turns (non-interactive; use conversation ids to continue). discovery The registry: search cards, get recommendations, resolve a task to a tool, read a card. models The models plane (your ozk_ key): catalog, chat and text completions, embeddings, generation cost, credits, key, endpoints. mcp MCP: the platform tool list, and outbound runs against connected MCP servers. traces Execution traces: spans and gateway requests by trace id. keys One API keys (ozk_) — needs a Studio session. balance [options] Prepaid credit balance (needs a Studio session). usage [options] Usage and cost rollups (needs a Studio session). nodes Local nodes (device daemons) — needs a Studio session. campaigns Campaign ledgers on a workflow — needs a Studio session. connections Provider connections (API keys, OAuth) — needs a Studio session; `connections api …` is the same door over the API key. secrets Developer secrets (encrypted at rest, scoped to a company, team or project) — needs a Studio session. documents Stored documents — needs a Studio session. teams Teams — needs a Studio session. projects Projects — needs a Studio session. skills Install, update and manage PlungeAI agent skills from skills.plungeai.com. completion Shell completion for bash, zsh and fish. update [options] Update to the latest published version (npm). ``` ## ocean-cli login ``` Usage: ocean-cli login [options] Sign in through the browser and store your account key (ozk_…), which opens every plane, or with --session the optional Studio session. Options: --key <source> Read the secret from "-" (stdin) or a file; omitted = sign in through the browser in a terminal --session Store the Studio auth-session cookie value instead of an API key --no-browser Paste a key at a hidden prompt instead of signing in through the browser --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli login # opens the browser to sign in, stores your account key ocean-cli login --no-browser # paste a key from https://dashboard.plungeai.com (One API → Keys) echo "$OCEAN_API_KEY" | ocean-cli login --key - ocean-cli login --session --key cookie.txt ``` ## ocean-cli logout ``` Usage: ocean-cli logout [options] Remove stored credentials. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli logout ``` ## ocean-cli auth ``` Usage: ocean-cli auth [options] Check authentication status. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli auth --json ``` ## ocean-cli whoami ``` Usage: ocean-cli whoami [options] Show the identity, tier and rate window behind the API key. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli whoami --json ``` ## ocean-cli spec ``` Usage: ocean-cli spec [options] The One API OpenAPI document (GET /v1/openapi.json); no key needed. Options: --json Output as JSON -o, --output <path> Save the document to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli spec -o openapi.json ``` ## ocean-cli doctor ``` Usage: ocean-cli doctor [options] Live connectivity and credential checks (exit 4 when any fails). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli doctor ``` ## ocean-cli config ``` Usage: ocean-cli config [options] [key] [value] View or set CLI configuration. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Keys: api_url, mcp_url, studio_url, dashboard_url, default_provider Examples: ocean-cli config # show all settings ocean-cli config default_provider tavily ocean-cli config api_url https://api.plungeai.com ``` ## ocean-cli search ``` Usage: ocean-cli search [options] [objective] Search the web with PlungeAI's search agents (Parallel by default). Options: -q, --query <text> Keyword search query (can be repeated) (default: []) --mode <mode> Search mode (Parallel) (choices: "turbo", "fast", "basic", "advanced") --max-results <n> Maximum results --include-domains <list> Only search these domains (comma-separated or repeated) (default: []) --exclude-domains <list> Exclude these domains (comma-separated or repeated) (default: []) --after-date <date> Only results after this date (YYYY-MM-DD) --excerpt-max-chars-per-result <n> Max characters per result for excerpts (min 1000) --excerpt-max-chars-total <n> Max total characters for excerpts --max-age-seconds <n> Max age in seconds before fetching live content (min 600) --timeout-seconds <n> Timeout in seconds for fetching live content --disable-cache-fallback Return an error instead of stale cached content --location <cc> ISO 3166-1 alpha-2 country code for geo-targeted results (e.g. us, gb, de) --provider <name> Search agent (default: config default_provider or parallel) (choices: "parallel", "tavily", "firecrawl", "serper", "brave", "exa") --dry-run Show what would be sent without making API calls --session-id <id> Session ID to group related search/extract calls --client-model <model> The model generating this request and consuming the results --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli search "Who is the current Head of Sales at Adyen?" --max-results 3 ocean-cli search -q "adyen head of sales" -q "adyen VP sales" --json echo "latest Cloudflare Workers pricing" | ocean-cli search - --provider tavily ``` ## ocean-cli extract ``` Usage: ocean-cli extract [options] <urls...> Extract content from URLs as clean markdown. Options: --objective <text> Focus extraction on a specific goal -q, --query <text> Keywords to prioritize (can be repeated) (default: []) --full-content Include complete page content --full-content-max-chars <n> Max characters per result for full content --excerpt-max-chars-per-result <n> Max characters per result for excerpts (min 1000) --excerpt-max-chars-total <n> Max total characters for excerpts across all URLs --max-age-seconds <n> Max age in seconds before fetching live content (min 600) --timeout-seconds <n> Timeout in seconds for fetching live content --disable-cache-fallback Return an error instead of stale cached content --provider <name> Extraction agent (default: config default_provider when it can extract, else parallel) (choices: "parallel", "tavily", "firecrawl") --dry-run Show what would be sent without making API calls --session-id <id> Session ID to group related search/extract calls --client-model <model> The model generating this request and consuming the results --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli extract https://docs.parallel.ai/integrations/cli --objective "install steps" ocean-cli extract https://a.com https://b.com --full-content --json ocean-cli extract https://a.com --provider firecrawl ``` ## ocean-cli answer ``` Usage: ocean-cli answer [options] [question] A reasoning-enhanced, cited answer from the web (Parallel Answer). Options: --effort <level> Reasoning effort (choices: "low", "medium", "high") --instructions <text> Additional instructions for the response --include-domains <list> Only cite these domains (comma-separated or repeated) (default: []) --exclude-domains <list> Never cite these domains (comma-separated or repeated) (default: []) --max-cost <usd> Cost cap in USD (default 2.00) --dry-run Show what would be sent without making API calls --session-id <id> Session ID to group related search/extract calls --client-model <model> The model generating this request and consuming the results --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli answer "What changed in Cloudflare Workers pricing in 2026?" --effort medium echo "question" | ocean-cli answer - --json ``` ## ocean-cli research ``` Usage: ocean-cli research [options] [command] Deep research for open-ended questions (Parallel Task API). Options: -h, --help display help for command Commands: run [options] [query] Run deep research on a question or topic. Results are always saved to disk (default ./ocean-research/<run_id>.json). status [options] <run_id> Check the status of a research task. poll [options] <run_id> Poll an existing research task until completion and save the result (default ./ocean-research/<run_id>.json). result [options] <run_id> Fetch the result of a completed research task (no file written unless -o). processors [options] List available research processors and their characteristics. ``` ## ocean-cli research run ``` Usage: ocean-cli research run [options] [query] Run deep research on a question or topic. Results are always saved to disk (default ./ocean-research/<run_id>.json). Options: -f, --input-file <path> Read the query from a file -p, --processor <tier> Processor tier (higher = more thorough but slower) (choices: "lite-fast", "base-fast", "core-fast", "core2x-fast", "pro-fast", "ultra-fast", "ultra2x-fast", "ultra4x-fast", "ultra8x-fast", "lite", "base", "core", "core2x", "pro", "ultra", "ultra2x", "ultra4x", "ultra8x", default: "pro-fast") --text Return a markdown report (text schema) instead of structured JSON --text-description <text> Steering description for the markdown report (implies --text, e.g. 'Keep under 1000 words') --output-schema <json> JSON schema for structured output (inline, - for stdin, @file) --include-domains <list> Only use these domains (comma-separated or repeated) (default: []) --exclude-domains <list> Exclude these domains (comma-separated or repeated) (default: []) --after-date <date> Only results after this date (YYYY-MM-DD) --location <cc> ISO 3166-1 alpha-2 country code --previous-interaction-id <id> Continue from a prior run --memory-scope-key <key> Memory scope for this run --max-cost <usd> Cost cap in USD (default 2.00) --no-wait Return immediately with the id (don't poll) --timeout <seconds> Max wait time in seconds (default: 3600) --poll-interval <seconds> Seconds between status checks (default: 45) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Base path — writes <base>.json (and <base>.md with --text) --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli research run "What are the latest developments in quantum computing?" ocean-cli research run --text "Market analysis of HVAC industry" -o report ocean-cli research run -f question.txt --processor ultra --text -o report echo "My research question" | ocean-cli research run - --json ocean-cli research run "Follow-up?" --previous-interaction-id trun_abc123 ``` ## ocean-cli research status ``` Usage: ocean-cli research status [options] <run_id> Check the status of a research task. Options: --json Output as JSON -o, --output <path> Base path — writes <base>.json --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli research status trun_abc123 ``` ## ocean-cli research poll ``` Usage: ocean-cli research poll [options] <run_id> Poll an existing research task until completion and save the result (default ./ocean-research/<run_id>.json). Options: --timeout <seconds> Max wait time in seconds (default: 3600) --poll-interval <seconds> Seconds between status checks (default: 45) --json Output as JSON -o, --output <path> Base path — writes <base>.json --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli research poll trun_abc123 -o report ``` ## ocean-cli research result ``` Usage: ocean-cli research result [options] <run_id> Fetch the result of a completed research task (no file written unless -o). Options: --json Output as JSON -o, --output <path> Base path — writes <base>.json --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli research result trun_abc123 --json ``` ## ocean-cli research processors ``` Usage: ocean-cli research processors [options] List available research processors and their characteristics. Options: --json Output as JSON -o, --output <path> Base path — writes <base>.json --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli research processors ``` ## ocean-cli findall ``` Usage: ocean-cli findall [options] [command] FindAll: discover entities from the web using natural language. Options: -h, --help display help for command Commands: run [options] <objective> Run a FindAll query to discover and match entities from the web. Results are always saved to disk (default ./ocean-findall/<findall_id>.json). status [options] <findall_id> Check the status of a FindAll run. poll [options] <findall_id> Poll a FindAll run until completion, save and show the results (default ./ocean-findall/<findall_id>.json). result [options] <findall_id> Fetch results of a completed FindAll run. cancel [options] <findall_id> Cancel a running FindAll run. ingest [options] <objective> Convert a natural language objective into a FindAll schema (preview before running). entity-search [options] <objective> Return ranked entities matching a natural language objective (best-effort, low latency). extend [options] <findall_id> Extend a finished FindAll run to get more matches. enrich [options] <findall_id> Add an enrichment to every match of a FindAll run. schema [options] <findall_id> Read a FindAll run's current spec. ``` ## ocean-cli findall run ``` Usage: ocean-cli findall run [options] <objective> Run a FindAll query to discover and match entities from the web. Results are always saved to disk (default ./ocean-findall/<findall_id>.json). Options: -g, --generator <tier> Generator tier (higher = more thorough but slower/costlier) (choices: "preview", "base", "core", "pro", default: "core") -n, --match-limit <n> Maximum number of matched candidates (5-1000) (default: 10) --entity-type <type> Entity type (default companies) --exclude <json> Entities to exclude as JSON array of {name, url} objects (inline, - or @file) --enrichment-schema <json> JSON schema for per-match enrichment (inline, - or @file) --enrichment-processor <tier> Processor for enrichment tasks --include-unmatched Also return every candidate that did not match --memory-scope-key <key> Memory scope for this run --metadata <json> Metadata as JSON string --max-cost <usd> Cost cap in USD (default 2.00) --no-wait Return immediately with the id (don't poll) --timeout <seconds> Max wait time in seconds (default: 3600) --poll-interval <seconds> Seconds between status checks (default: 30) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli findall run "Find all AI companies that raised Series A in 2026" ocean-cli findall run "Find roofing companies in Charlotte NC" -g base -n 25 ocean-cli findall run "Find YC companies in developer tools" --no-wait --json ocean-cli findall run "Find AI startups" --exclude '[{"name": "OpenAI", "url": "openai.com"}]' ``` ## ocean-cli findall status ``` Usage: ocean-cli findall status [options] <findall_id> Check the status of a FindAll run. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli findall status findall_abc ``` ## ocean-cli findall poll ``` Usage: ocean-cli findall poll [options] <findall_id> Poll a FindAll run until completion, save and show the results (default ./ocean-findall/<findall_id>.json). Options: --timeout <seconds> Max wait time in seconds (default: 3600) --poll-interval <seconds> Seconds between status checks (default: 30) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli findall poll findall_abc -o out.json ``` ## ocean-cli findall result ``` Usage: ocean-cli findall result [options] <findall_id> Fetch results of a completed FindAll run. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli findall result findall_abc --json ``` ## ocean-cli findall cancel ``` Usage: ocean-cli findall cancel [options] <findall_id> Cancel a running FindAll run. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli findall cancel findall_abc ``` ## ocean-cli findall ingest ``` Usage: ocean-cli findall ingest [options] <objective> Convert a natural language objective into a FindAll schema (preview before running). Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli findall ingest "Find all AI companies that raised Series A in 2026" ``` ## ocean-cli findall entity-search ``` Usage: ocean-cli findall entity-search [options] <objective> Return ranked entities matching a natural language objective (best-effort, low latency). Options: -t, --entity-type <type> Entity type to search for (choices: "companies", "people") -n, --match-limit <n> Maximum entities to return (5-1000) (default: 10) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli findall entity-search "AI startups in San Francisco" -t companies ocean-cli findall entity-search "Senior PMs at AI startups" -t people -n 25 ``` ## ocean-cli findall extend ``` Usage: ocean-cli findall extend [options] <findall_id> Extend a finished FindAll run to get more matches. Options: -n, --additional <n> Additional matches to find --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli findall extend findall_abc -n 20 ``` ## ocean-cli findall enrich ``` Usage: ocean-cli findall enrich [options] <findall_id> Add an enrichment to every match of a FindAll run. Options: --schema <json> Enrichment JSON schema (inline, - or @file) --processor <tier> Processor for the enrichment tasks --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli findall enrich findall_abc --schema '{"type":"object","properties":{"ceo":{"type":"string"}}}' ``` ## ocean-cli findall schema ``` Usage: ocean-cli findall schema [options] <findall_id> Read a FindAll run's current spec. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli findall schema findall_abc --json ``` ## ocean-cli monitor ``` Usage: ocean-cli monitor [options] [command] Monitor: continuously track the web for changes. Options: -h, --help display help for command Commands: create [options] [query] Create a monitor. QUERY is the search query for type=event_stream (default); for type=snapshot omit QUERY and pass --task-run-id. list [options] List monitors (newest first). get [options] <monitor_id> Get details of a specific monitor. update [options] <monitor_id> Update an existing monitor (query and task_run_id are immutable — create a new monitor to change them). cancel [options] <monitor_id> Cancel a monitor (irreversible). trigger [options] <monitor_id> Trigger an immediate one-off (billed) run of a monitor. events [options] <monitor_id> List events for a monitor (newest first). ``` ## ocean-cli monitor create ``` Usage: ocean-cli monitor create [options] [query] Create a monitor. QUERY is the search query for type=event_stream (default); for type=snapshot omit QUERY and pass --task-run-id. Options: -f, --frequency <spec> How often to run: <n><unit> with unit h/d/w (e.g. 1h, 6h, 1d, 2w) or hourly/daily/weekly (default: "1d") --type <type> Monitor type (choices: "event_stream", "snapshot", default: "event_stream") --task-run-id <id> Required for type=snapshot: the Task Run whose output to track --processor <tier> Monitor processor (default lite; 'base' is more thorough at higher cost) (choices: "lite", "base") --webhook <url> Webhook URL for event delivery --metadata <json> Metadata as JSON (inline, - or @file) --output-schema <json> Output schema as JSON (event_stream only) --include-backfill event_stream only: include a sample of historical events on first run --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli monitor create "Track price changes for iPhone 16" ocean-cli monitor create "New AI funding announcements" --frequency 1h ocean-cli monitor create --type snapshot --task-run-id trun_abc --frequency 1d Note: monitor creation is a gated (billed, recurring) operation — the unattended API refuses it (HTTP 403, reported as approval_required, exit 4) until the owner approves it. ``` ## ocean-cli monitor list ``` Usage: ocean-cli monitor list [options] List monitors (newest first). Options: -n, --limit <n> Maximum number of monitors to return (1-10000) --cursor <token> Pagination token from a previous response --status <status> Filter by status (repeatable). Defaults to active only. (choices: "active", "cancelled") --type <type> Filter by monitor type (repeatable) (choices: "event_stream", "snapshot") --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli monitor list ocean-cli monitor list --limit 10 --json ocean-cli monitor list --status active --status cancelled ``` ## ocean-cli monitor get ``` Usage: ocean-cli monitor get [options] <monitor_id> Get details of a specific monitor. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli monitor get mon_abc ``` ## ocean-cli monitor update ``` Usage: ocean-cli monitor update [options] <monitor_id> Update an existing monitor (query and task_run_id are immutable — create a new monitor to change them). Options: -f, --frequency <spec> Updated frequency (e.g. 1h, 6h, 1d, 2w, or hourly/daily/weekly) --processor <tier> Updated processor (choices: "lite", "base") --webhook <url> Updated webhook URL --metadata <json> Updated metadata as JSON (inline, - or @file) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli monitor update mon_abc --frequency 1h ocean-cli monitor update mon_abc --webhook https://example.com/hook ``` ## ocean-cli monitor cancel ``` Usage: ocean-cli monitor cancel [options] <monitor_id> Cancel a monitor (irreversible). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli monitor cancel mon_abc ``` ## ocean-cli monitor trigger ``` Usage: ocean-cli monitor trigger [options] <monitor_id> Trigger an immediate one-off (billed) run of a monitor. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli monitor trigger mon_abc ``` ## ocean-cli monitor events ``` Usage: ocean-cli monitor events [options] <monitor_id> List events for a monitor (newest first). Options: --limit <n> Maximum number of events to return (1-100, default 20) --cursor <token> Pagination token from a previous response --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli monitor events mon_abc ocean-cli monitor events mon_abc --limit 50 --json ``` ## ocean-cli enrich ``` Usage: ocean-cli enrich [options] [command] Data enrichment: add web-researched columns to a list of rows. Options: -h, --help display help for command Commands: run [options] [config] Run data enrichment from a YAML config or CLI arguments (rows from --data, --source CSV/JSON, or the config). suggest [options] <intent> Use AI to suggest output columns for an enrichment. status [options] <taskgroup_id> Check the status of an enrichment task group. poll [options] <taskgroup_id> Poll an enrichment task group until completion and save its runs (default ./ocean-enrich/<taskgroup_id>.json; rows come back without the source columns). ``` ## ocean-cli enrich run ``` Usage: ocean-cli enrich run [options] [config] Run data enrichment from a YAML config or CLI arguments (rows from --data, --source CSV/JSON, or the config). Options: --source <path> Source rows: a .csv or .json file --target <path> Write the enriched rows here (.json writes JSON, anything else CSV) --data <json> Inline JSON array of rows, - or @file (alternative to --source) --intent <text> Natural-language description; AI suggests the output columns when --enriched-columns is omitted --enriched-columns <json> Output columns as JSON: [{"name","description","type"}] --source-columns <json> Source columns as JSON: [{"name","description"}] (default: inferred from the rows) --processor <tier> Processor to use (default base) (choices: "lite-fast", "base-fast", "core-fast", "core2x-fast", "pro-fast", "ultra-fast", "ultra2x-fast", "ultra4x-fast", "ultra8x-fast", "lite", "base", "core", "core2x", "pro", "ultra", "ultra2x", "ultra4x", "ultra8x") --no-wait Return immediately with the id (don't poll) --timeout <seconds> Max wait time in seconds (default: 3600) --poll-interval <seconds> Seconds between status checks (default: 5) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the full JSON result here (default ./ocean-enrich/<taskgroup_id>.json without --target) --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli enrich run config.yaml ocean-cli enrich run --source data.csv --intent "Find the CEO" --target out.csv ocean-cli enrich run --data '[{"company": "Google"}, {"company": "Apple"}]' --enriched-columns '[{"name":"ceo","description":"CEO name"}]' YAML config keys: source_type, source, target, processor, intent, source_columns, enriched_columns ``` ## ocean-cli enrich suggest ``` Usage: ocean-cli enrich suggest [options] <intent> Use AI to suggest output columns for an enrichment. Options: --source-columns <json> Source columns as JSON: [{"name","description"}] --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli enrich suggest "Find the CEO and founding year" --source-columns '[{"name":"company"}]' ``` ## ocean-cli enrich status ``` Usage: ocean-cli enrich status [options] <taskgroup_id> Check the status of an enrichment task group. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli enrich status tgrp_abc ``` ## ocean-cli enrich poll ``` Usage: ocean-cli enrich poll [options] <taskgroup_id> Poll an enrichment task group until completion and save its runs (default ./ocean-enrich/<taskgroup_id>.json; rows come back without the source columns). Options: --timeout <seconds> Max wait time in seconds (default: 3600) --poll-interval <seconds> Seconds between status checks (default: 5) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli enrich poll tgrp_abc --json ``` ## ocean-cli agents ``` Usage: ocean-cli agents [options] [command] Registry agents: list, inspect and run them (agents plane). Options: -h, --help display help for command Commands: list [options] List active agents (with --search/--category the discovery index is used). categories [options] List agent categories with counts. get [options] <agent_id> Show an agent's card (markdown with YAML frontmatter). run [options] <agent_id> [prompt] Run one agent with a prompt (sync; --stream for live tokens). Async agent work: missions run, tools call --async. result [options] <workflow_id> [task_id] Read the stored result of an agent execution (task_id defaults to t1). ``` ## ocean-cli agents list ``` Usage: ocean-cli agents list [options] List active agents (with --search/--category the discovery index is used). Options: --search <q> Free-text search --category <name> Filter by category --limit <n> Page size (max 100) (default: 50) --offset <n> Page offset (default: 0) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli agents list ocean-cli agents list --search "web search" --category search --json ``` ## ocean-cli agents categories ``` Usage: ocean-cli agents categories [options] List agent categories with counts. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli agents categories ``` ## ocean-cli agents get ``` Usage: ocean-cli agents get [options] <agent_id> Show an agent's card (markdown with YAML frontmatter). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli agents get one-agent ``` ## ocean-cli agents run ``` Usage: ocean-cli agents run [options] <agent_id> [prompt] Run one agent with a prompt (sync; --stream for live tokens). Async agent work: missions run, tools call --async. Options: --model <id> Model id (send --provider with it) --provider <name> Provider (google is normalised to gemini) --persona <id> Persona card id --system <text> System prompt --max-tokens <n> Max output tokens --temperature <n> Sampling temperature (0-2) --top-p <n> Nucleus sampling (0-1) --reasoning-effort <level> Reasoning effort (choices: "low", "medium", "high", "xhigh") --stream Stream tokens as they arrive (OpenAI chunk format; --json prints one chunk per line) --no-context Do not send your time zone and locale (user_context) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file (with --stream: the assembled content) --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli agents run one-agent "Summarise the Cloudflare Workers pricing page in 3 bullets" ocean-cli agents run one-agent "hello" --model claude-sonnet-4-6 --provider anthropic --stream echo "prompt" | ocean-cli agents run research-agent - --json ``` ## ocean-cli agents result ``` Usage: ocean-cli agents result [options] <workflow_id> [task_id] Read the stored result of an agent execution (task_id defaults to t1). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli agents result <workflow_id> ocean-cli agents result <workflow_id> t1 --json ``` ## ocean-cli tools ``` Usage: ocean-cli tools [options] [command] Structured tool agents: contracts and typed calls (tools plane). Options: -h, --help display help for command Commands: list [options] List callable tool agents. get [options] <agent_id> Show a tool agent's invocation contract (operations, required params, examples). call [options] <agent_id> [operation] Execute one operation of a tool agent with typed params. ``` ## ocean-cli tools list ``` Usage: ocean-cli tools list [options] List callable tool agents. Options: --limit <n> Page size (max 100) (default: 50) --offset <n> Page offset (default: 0) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli tools list ocean-cli tools list --limit 100 --json ``` ## ocean-cli tools get ``` Usage: ocean-cli tools get [options] <agent_id> Show a tool agent's invocation contract (operations, required params, examples). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli tools get parallel-agent ``` ## ocean-cli tools call ``` Usage: ocean-cli tools call [options] <agent_id> [operation] Execute one operation of a tool agent with typed params. Options: --params <json> Params as a JSON object (inline, - for stdin, @file) --prompt <text> Free-text prompt for agents that take one --async Dispatch through MCP and return an execution id (poll with executions status) --no-context With --async: do not send your time zone and locale (user_context) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli tools call parallel-agent search --params '{"query":"Head of Sales at Adyen","max_results":3}' ocean-cli tools call serper-agent news --params @params.json --json ocean-cli tools call tavily-agent research --params '{"query":"x"}' --async ``` ## ocean-cli workflows ``` Usage: ocean-cli workflows [options] [command] Saved workflows (CNL): list, create, run, versions. Options: -h, --help display help for command Commands: list [options] List your workflows. get [options] <id> Show one workflow (or its YAML with --yaml). create [options] <name> <file> Save a workflow from a YAML file (validated and policy-linted first). update [options] <id> Update a workflow (YAML file, name, description, kind or folder). delete [options] <id> Delete a workflow. versions [options] <id> List saved versions. restore [options] <id> <version_id> Restore a saved version. validate [options] <file> Validate a workflow YAML locally (CNL schema + save-time policy lint). run [options] <target> Run a saved workflow by id, or a YAML file inline. Waits for the result by default. result [options] <workflow_id> <task_id> Read a stored task result. cancel [options] <execution_id> Cancel a running execution (cooperative). permissions A bot's permission presets, admin locks and spend caps. files [options] <id> A bot's persistent workspace files; --download PATH fetches one. inbox [options] <id> A bot's queued bot-to-bot messages, newest first (read-only). bindings Paired chats (Telegram, WhatsApp, Discord, Teams) that talk to a bot. save-version [options] <id> Save the current YAML as a new version. build [options] Have the platform design a workflow from a goal, or edit one from an instruction. ``` ## ocean-cli workflows list ``` Usage: ocean-cli workflows list [options] List your workflows. Options: --search <q> Name search --folder <name> Folder --kind <kind> Only this kind (filters the fetched rows) (choices: "workflow", "agent", "bot", "campaign") --limit <n> Max rows (1-100) (default: 50) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli workflows list --search market --json ocean-cli workflows list --kind bot ``` ## ocean-cli workflows get ``` Usage: ocean-cli workflows get [options] <id> Show one workflow (or its YAML with --yaml). Options: --yaml Print the workflow YAML --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli workflows get <id> ocean-cli workflows get <id> --yaml > flow.yaml ``` ## ocean-cli workflows create ``` Usage: ocean-cli workflows create [options] <name> <file> Save a workflow from a YAML file (validated and policy-linted first). Options: --description <text> Description --kind <kind> Kind (choices: "workflow", "agent", "bot") --folder <name> Folder --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli workflows create "Market Monitor" flow.yaml --kind workflow ocean-cli workflows create "Market Monitor" flow.yaml --dry-run ``` ## ocean-cli workflows update ``` Usage: ocean-cli workflows update [options] <id> Update a workflow (YAML file, name, description, kind or folder). Options: -f, --file <path> New YAML --name <text> New name --description <text> New description --kind <kind> New kind (choices: "workflow", "agent", "bot") --folder <name> Move into this folder ("none" clears it) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli workflows update <id> -f flow.yaml ocean-cli workflows update <id> --name "Market Monitor v2" --dry-run ocean-cli workflows update <id> --kind bot --folder Bots ``` ## ocean-cli workflows delete ``` Usage: ocean-cli workflows delete [options] <id> Delete a workflow. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows delete <id> --yes ``` ## ocean-cli workflows versions ``` Usage: ocean-cli workflows versions [options] <id> List saved versions. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows versions <id> ``` ## ocean-cli workflows restore ``` Usage: ocean-cli workflows restore [options] <id> <version_id> Restore a saved version. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows restore <id> <version_id> --yes ``` ## ocean-cli workflows validate ``` Usage: ocean-cli workflows validate [options] <file> Validate a workflow YAML locally (CNL schema + save-time policy lint). Options: --kind <kind> Kind (choices: "workflow", "agent", "bot", "campaign") --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows validate flow.yaml --kind bot ``` ## ocean-cli workflows run ``` Usage: ocean-cli workflows run [options] <target> Run a saved workflow by id, or a YAML file inline. Waits for the result by default. Options: -i, --input <text> Single input value ("-" reads stdin) --inputs <file.json> Named inputs: a flat JSON object of strings --stream Stream the live engine events on stdout, then the result (with --json: one JSON document per line) --no-context Do not send your time zone and locale (user_context) --no-wait Return immediately with the id (don't poll) --timeout <seconds> Max wait time in seconds (default: 3600) --poll-interval <seconds> Seconds between status checks (default: 5) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli workflows run <id> -i "AI startups" ocean-cli workflows run flow.yaml --inputs inputs.json --json ocean-cli workflows run <id> -i "x" --stream ocean-cli workflows run <id> -i "x" --no-wait ``` ## ocean-cli workflows result ``` Usage: ocean-cli workflows result [options] <workflow_id> <task_id> Read a stored task result. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows result <workflow_id> <task_id> ``` ## ocean-cli workflows cancel ``` Usage: ocean-cli workflows cancel [options] <execution_id> Cancel a running execution (cooperative). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows cancel <execution_id> ``` ## ocean-cli workflows permissions ``` Usage: ocean-cli workflows permissions [options] [command] A bot's permission presets, admin locks and spend caps. Options: -h, --help display help for command Commands: get [options] <id> Show the stored policy. set [options] <id> Set presets, locks and spend caps; every flag changes only what it names. ``` ## ocean-cli workflows permissions get ``` Usage: ocean-cli workflows permissions get [options] <id> Show the stored policy. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows permissions get <bot_id> --json ``` ## ocean-cli workflows permissions set ``` Usage: ocean-cli workflows permissions set [options] <id> Set presets, locks and spend caps; every flag changes only what it names. Options: --class <class=preset> Preset for a class, repeatable: class send|write|pay|delete, preset allow|ask|deny|hand_off (default: []) --lock <class> Lock a class (team admins only): the bot's own allow is forced up to ask; repeatable (default: []) --unlock <class> Release a lock (team admins only); repeatable (default: []) --budget-run <usd|none> Per-run spend cap in USD ("none" clears it) --budget-month <usd|none> Monthly spend cap in USD ("none" clears it) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli workflows permissions set <bot_id> --class pay=ask --class send=allow ocean-cli workflows permissions set <bot_id> --lock pay --budget-run 0.5 --budget-month 20 ocean-cli workflows permissions set <bot_id> --budget-run none --dry-run ``` ## ocean-cli workflows files ``` Usage: ocean-cli workflows files [options] <id> A bot's persistent workspace files; --download PATH fetches one. Options: --download <path> Fetch this listed file (raw bytes) instead of listing --out <file> With --download: write the file here (without it the bytes go to stdout when it is not a terminal) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli workflows files <bot_id> ocean-cli workflows files <bot_id> --download notes/a.md --out a.md ``` ## ocean-cli workflows inbox ``` Usage: ocean-cli workflows inbox [options] <id> A bot's queued bot-to-bot messages, newest first (read-only). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows inbox <bot_id> ``` ## ocean-cli workflows bindings ``` Usage: ocean-cli workflows bindings [options] [command] Paired chats (Telegram, WhatsApp, Discord, Teams) that talk to a bot. Options: -h, --help display help for command Commands: list [options] <id> Chats bound to the bot. bind [options] <id> Bind a paired chat to the bot (a chat talks to one bot: binding it again moves it). unbind [options] <id> Release a chat from the bot. ``` ## ocean-cli workflows bindings list ``` Usage: ocean-cli workflows bindings list [options] <id> Chats bound to the bot. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows bindings list <bot_id> ``` ## ocean-cli workflows bindings bind ``` Usage: ocean-cli workflows bindings bind [options] <id> Bind a paired chat to the bot (a chat talks to one bot: binding it again moves it). Options: --channel <name> telegram, whatsapp, discord or teams --sender <id> The paired chat's sender id (from the pairing) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows bindings bind <bot_id> --channel telegram --sender <sender_id> ``` ## ocean-cli workflows bindings unbind ``` Usage: ocean-cli workflows bindings unbind [options] <id> Release a chat from the bot. Options: --channel <name> telegram, whatsapp, discord or teams --sender <id> The bound chat's sender id --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows bindings unbind <bot_id> --channel telegram --sender <sender_id> --yes ``` ## ocean-cli workflows save-version ``` Usage: ocean-cli workflows save-version [options] <id> Save the current YAML as a new version. Options: --label <text> A label for the version (up to 200 characters) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli workflows save-version <id> --label "before the Slack change" ``` ## ocean-cli workflows build ``` Usage: ocean-cli workflows build [options] Have the platform design a workflow from a goal, or edit one from an instruction. Options: --goal <text> What the workflow should do --workflow-id <id> Existing workflow to edit --instruction <text> What to change --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli workflows build --goal "watch AI funding news and email me daily" ocean-cli workflows build --workflow-id <id> --instruction "add a summary step" ``` ## ocean-cli bot-groups ``` Usage: ocean-cli bot-groups [options] [command] Bot groups: named sets of 2 to 6 of your saved bots (message_bot to=group:<name>). Options: -h, --help display help for command Commands: list [options] List your bot groups. create [options] Create a bot group. update [options] <id> Replace a bot group (name and members). delete [options] <id> Delete a bot group (the member bots are untouched). thread [options] <id> A bot group's shared thread, oldest first. ``` ## ocean-cli bot-groups list ``` Usage: ocean-cli bot-groups list [options] List your bot groups. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli bot-groups list --json ``` ## ocean-cli bot-groups create ``` Usage: ocean-cli bot-groups create [options] Create a bot group. Options: --name <name> 1-40 letters, digits, "_", "." or "-"; unique per account --members <ids> Your saved-bot ids, 2 to 6 (comma-separated or repeated) (default: []) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli bot-groups create --name research-crew --members <bot_id>,<bot_id> ``` ## ocean-cli bot-groups update ``` Usage: ocean-cli bot-groups update [options] <id> Replace a bot group (name and members). Options: --name <name> New name --members <ids> Your saved-bot ids, 2 to 6 (comma-separated or repeated) (default: []) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli bot-groups update <group_id> --name research-crew --members <bot_id>,<bot_id>,<bot_id> ``` ## ocean-cli bot-groups delete ``` Usage: ocean-cli bot-groups delete [options] <id> Delete a bot group (the member bots are untouched). Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli bot-groups delete <group_id> --yes ``` ## ocean-cli bot-groups thread ``` Usage: ocean-cli bot-groups thread [options] <id> A bot group's shared thread, oldest first. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli bot-groups thread <group_id> ``` ## ocean-cli call ``` Usage: ocean-cli call [options] <file> Agent Call: POST a program — a building block (agent: brave-agent + its fields), a workflow, or a saved agent (agent: <id>[@n] + input) — to /v1/execute. "-" reads it from stdin. Options: --dry-run Ask the platform what would run (dry_run: true — lint, no row, no run) --stream Stream the live engine events on stdout, then the result (with --json: one JSON document per line) --on-behalf-of <subject> Run as one of your customers: a connection-tier agent uses the account connected under this id --json Output as JSON -o, --output <path> Also save the JSON result to this file (with --stream: the result) --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli call program.yaml ocean-cli call program.yaml --dry-run --json cat program.yaml | ocean-cli call - --stream ``` ## ocean-cli companies ``` Usage: ocean-cli companies [options] [command] Companies in YAML: apply a campus, run its clock, put items into buildings, decide at the door. Options: -h, --help display help for command Commands: list [options] Your companies: one row per applied campus. apply [options] <files...> Save the campus, building and office documents (offices first), then compile and store the plan. Idempotent by name; does not start the clock. layout [options] <campus> The compiled company: buildings, floors, offices, pipelines. snapshot [options] <campus> The company now: items, approvals, the clock. slice [options] <campus> <building> A local building's slice: what its node runs. events [options] <campus> The company's event log: a page, or --follow for the live feed (one line per event; with --json one JSON document per line). ingest [options] <campus> <file> Report events from a local building: a YAML or JSON file {events: [{building, type, …}]} (or a bare list). items A building's items: create, get, pause, resume, retry, cancel, steer. approvals [options] <campus> The door: gates waiting on a person and tool permissions an office run asked for. approve [options] <campus> <ap> Approve an approval at the door. reject [options] <campus> <ap> Reject an approval at the door. mode [options] <campus> <mode> Set the company mode: supervised | tiered | autonomous. start [options] <campus> Start the company's clock (offices run and spend from here on). stop [options] <campus> Stop the company's clock. delete [options] <campus> Delete a stopped company and every row of it (409 while it runs). The documents stay. ``` ## ocean-cli companies list ``` Usage: ocean-cli companies list [options] Your companies: one row per applied campus. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies list --json ``` ## ocean-cli companies apply ``` Usage: ocean-cli companies apply [options] <files...> Save the campus, building and office documents (offices first), then compile and store the plan. Idempotent by name; does not start the clock. Options: --campus <slug> The campus to apply when no campus document is among the files --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli companies apply campus.yaml ocean-cli companies apply plunge/campus.yaml plunge/*/building.yaml plunge/*/*.yaml ``` ## ocean-cli companies layout ``` Usage: ocean-cli companies layout [options] <campus> The compiled company: buildings, floors, offices, pipelines. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies layout plunge --json ``` ## ocean-cli companies snapshot ``` Usage: ocean-cli companies snapshot [options] <campus> The company now: items, approvals, the clock. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies snapshot plunge --json ``` ## ocean-cli companies slice ``` Usage: ocean-cli companies slice [options] <campus> <building> A local building's slice: what its node runs. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies slice plunge bench --json ``` ## ocean-cli companies events ``` Usage: ocean-cli companies events [options] <campus> The company's event log: a page, or --follow for the live feed (one line per event; with --json one JSON document per line). Options: --since <seq> Rows after this seq --limit <n> At most this many rows (page only, 1-1000) --follow Stay attached and print events as they happen --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli companies events plunge --since 120 ocean-cli companies events plunge --follow --json ``` ## ocean-cli companies ingest ``` Usage: ocean-cli companies ingest [options] <campus> <file> Report events from a local building: a YAML or JSON file {events: [{building, type, …}]} (or a bare list). Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies ingest plunge events.json ``` ## ocean-cli companies items ``` Usage: ocean-cli companies items [options] [command] A building's items: create, get, pause, resume, retry, cancel, steer. Options: -h, --help display help for command Commands: create [options] <campus> <file> Put an item into a building: a YAML or JSON file {building, title[, body, kind, priority, tier]}. get [options] <campus> <id> Read one item in full: its bodies and every run, approval and own event. pause [options] <campus> <id> Pause an item. resume [options] <campus> <id> Resume an item. retry [options] <campus> <id> Retry an item's step (a new attempt). cancel [options] <campus> <id> Cancel an item. steer [options] <campus> <id> <text> Steer an item: text its next step reads. ``` ## ocean-cli companies items create ``` Usage: ocean-cli companies items create [options] <campus> <file> Put an item into a building: a YAML or JSON file {building, title[, body, kind, priority, tier]}. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies items create plunge item.yaml ``` ## ocean-cli companies items get ``` Usage: ocean-cli companies items get [options] <campus> <id> Read one item in full: its bodies and every run, approval and own event. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies items get plunge <item_id> --json ``` ## ocean-cli companies items pause ``` Usage: ocean-cli companies items pause [options] <campus> <id> Pause an item. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies items pause plunge <item_id> ``` ## ocean-cli companies items resume ``` Usage: ocean-cli companies items resume [options] <campus> <id> Resume an item. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies items resume plunge <item_id> ``` ## ocean-cli companies items retry ``` Usage: ocean-cli companies items retry [options] <campus> <id> Retry an item's step (a new attempt). Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies items retry plunge <item_id> ``` ## ocean-cli companies items cancel ``` Usage: ocean-cli companies items cancel [options] <campus> <id> Cancel an item. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies items cancel plunge <item_id> ``` ## ocean-cli companies items steer ``` Usage: ocean-cli companies items steer [options] <campus> <id> <text> Steer an item: text its next step reads. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies items steer plunge <item_id> "Keep it under 50 words" ``` ## ocean-cli companies approvals ``` Usage: ocean-cli companies approvals [options] <campus> The door: gates waiting on a person and tool permissions an office run asked for. Options: --status <status> Only approvals in this state (choices: "pending", "approved", "rejected", "expired") --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies approvals plunge --status pending ``` ## ocean-cli companies approve ``` Usage: ocean-cli companies approve [options] <campus> <ap> Approve an approval at the door. Options: --note <text> A note for the record --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies approve plunge <approval_id> ``` ## ocean-cli companies reject ``` Usage: ocean-cli companies reject [options] <campus> <ap> Reject an approval at the door. Options: --note <text> A note for the record --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies reject plunge <approval_id> ``` ## ocean-cli companies mode ``` Usage: ocean-cli companies mode [options] <campus> <mode> Set the company mode: supervised | tiered | autonomous. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies mode plunge tiered ``` ## ocean-cli companies start ``` Usage: ocean-cli companies start [options] <campus> Start the company's clock (offices run and spend from here on). Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies start plunge ``` ## ocean-cli companies stop ``` Usage: ocean-cli companies stop [options] <campus> Stop the company's clock. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies stop plunge ``` ## ocean-cli companies delete ``` Usage: ocean-cli companies delete [options] <campus> Delete a stopped company and every row of it (409 while it runs). The documents stay. Options: --yes Confirm --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli companies delete plunge --yes ``` ## ocean-cli channels ``` Usage: ocean-cli channels [options] [command] Paired chat channels (Telegram, WhatsApp, Discord, Teams). Options: -h, --help display help for command Commands: list [options] Your paired channels; targets[].sender_id is what `workflows bindings bind` takes. pair [options] <channel> Mint a pairing code: send it to the channel's bot from the device (valid 10 minutes). ``` ## ocean-cli channels list ``` Usage: ocean-cli channels list [options] Your paired channels; targets[].sender_id is what `workflows bindings bind` takes. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli channels list --json ``` ## ocean-cli channels pair ``` Usage: ocean-cli channels pair [options] <channel> Mint a pairing code: send it to the channel's bot from the device (valid 10 minutes). Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli channels pair telegram ``` ## ocean-cli executions ``` Usage: ocean-cli executions [options] [command] Runs: list, status, poll, stream, output, continue, steer, files, export. Options: -h, --help display help for command Commands: stream [options] <id> Re-attach to a running execution (SSE): status, then progress of the tasks named by --task, until done (one line per event; with --json one JSON document per line). list [options] List recent executions. get [options] <id> Execution metadata (the status record). conversation [options] <id> The full multi-turn thread. cancel [options] <id> Cancel a running execution. output [options] <id> The result of an execution (platform markdown). delete [options] <id> Delete an execution and its stored result. status [options] <id> Current status (and any pending question or approval). poll [options] <id> Wait until the execution ends, then print its result. continue [options] <id> Answer a pending question (--message) or approve a pending action (--approve). followup [options] <id> <prompt> Ask a follow-up on a completed run (a running run gets the message queued). steer [options] <id> <text> Queue a message for a running execution (the live loop takes it at its next turn; a finished run is refused). files [options] <id> Files the run produced (spreadsheets, documents, decks). export [options] <id> Export the result as a document (needs a Studio session). Writes execution-<id>.<ext> unless -o. ``` ## ocean-cli executions stream ``` Usage: ocean-cli executions stream [options] <id> Re-attach to a running execution (SSE): status, then progress of the tasks named by --task, until done (one line per event; with --json one JSON document per line). Options: --task <ids> Task ids whose progress records to follow (comma-separated, at most 4) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions stream <execution_id> --task research,write --json ``` ## ocean-cli executions list ``` Usage: ocean-cli executions list [options] List recent executions. Options: --workflow <id> Only runs of this workflow --source <name> Only runs started from this source, e.g. mcp, api, studio, cli (filters the fetched rows) --limit <n> Max rows (1-100) (default: 20) --offset <n> Rows to skip (0-100000) (default: 0) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli executions list --limit 5 --json ocean-cli executions list --source cli ``` ## ocean-cli executions get ``` Usage: ocean-cli executions get [options] <id> Execution metadata (the status record). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions get <id> --json ``` ## ocean-cli executions conversation ``` Usage: ocean-cli executions conversation [options] <id> The full multi-turn thread. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions conversation <id> ``` ## ocean-cli executions cancel ``` Usage: ocean-cli executions cancel [options] <id> Cancel a running execution. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions cancel <id> ``` ## ocean-cli executions output ``` Usage: ocean-cli executions output [options] <id> The result of an execution (platform markdown). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions output <id> ``` ## ocean-cli executions delete ``` Usage: ocean-cli executions delete [options] <id> Delete an execution and its stored result. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions delete <id> --yes ``` ## ocean-cli executions status ``` Usage: ocean-cli executions status [options] <id> Current status (and any pending question or approval). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions status <id> --json ``` ## ocean-cli executions poll ``` Usage: ocean-cli executions poll [options] <id> Wait until the execution ends, then print its result. Options: --timeout <seconds> Max wait time in seconds (default: 3600) --poll-interval <seconds> Seconds between status checks (default: 5) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions poll <id> --timeout 600 -o result.json ``` ## ocean-cli executions continue ``` Usage: ocean-cli executions continue [options] <id> Answer a pending question (--message) or approve a pending action (--approve). Options: --message <text> Answer ("-" reads stdin) --approve Approve the pending action --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli executions continue <id> --message "EU region" ocean-cli executions continue <id> --approve ``` ## ocean-cli executions followup ``` Usage: ocean-cli executions followup [options] <id> <prompt> Ask a follow-up on a completed run (a running run gets the message queued). Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions followup <id> "Summarise the top three in one line each" ``` ## ocean-cli executions steer ``` Usage: ocean-cli executions steer [options] <id> <text> Queue a message for a running execution (the live loop takes it at its next turn; a finished run is refused). Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions steer <id> "Focus on the EU market" ``` ## ocean-cli executions files ``` Usage: ocean-cli executions files [options] <id> Files the run produced (spreadsheets, documents, decks). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli executions files <id> --json ``` ## ocean-cli executions export ``` Usage: ocean-cli executions export [options] <id> Export the result as a document (needs a Studio session). Writes execution-<id>.<ext> unless -o. Options: -f, --format <format> Format (choices: "docx", "pptx", "xlsx", "finmodel", "pdf", "package", "google-docs") --models <list> FinModel types (comma-separated; -f finmodel only) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> The file to write (google-docs: save the link JSON here) --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli executions export <id> -f docx -o report.docx ocean-cli executions export <id> -f finmodel --models dcf,lbo ocean-cli executions export <id> -f google-docs ``` ## ocean-cli missions ``` Usage: ocean-cli missions [options] [command] Bounded autonomous agent runs with memory (type: harness). Options: -h, --help display help for command Commands: run [options] <goal> Start a mission (async) and wait for it; the result is saved to ./ocean-missions/<id>.json (or -o). status [options] <id> Mission status (and any pending question or approval). poll [options] <id> Wait for a mission, then save and print its result (default ./ocean-missions/<id>.json). result [options] <id> The mission result (no file written unless -o). ``` ## ocean-cli missions run ``` Usage: ocean-cli missions run [options] <goal> Start a mission (async) and wait for it; the result is saved to ./ocean-missions/<id>.json (or -o). Options: --mission <text> Mission purpose statement --agent <id> Loop runtime agent (default one-agent) --tools <list> Allowed tools (comma-separated or repeated) (default: []) --max-iterations <n> Iteration cap (1-50) --criteria <text> Success criterion (repeatable) (default: []) --persona <id> Persona card id --skills <list> Skill card ids (comma-separated or repeated) (default: []) --sync Run on one connection and print the answer, instead of start + poll (a mission that pauses prints the executions continue hint) --no-context Do not send your time zone and locale (user_context) --no-wait Return immediately with the id (don't poll) --timeout <seconds> Max wait time in seconds (default: 600) --poll-interval <seconds> Seconds between status checks (default: 5) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> The result file (default ./ocean-missions/<id>.json) --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli missions run "Find three AI news items from today with links" --tools web_search,web_fetch,task_complete --max-iterations 5 ocean-cli missions run "Audit our pricing page" --criteria "lists every plan" --no-wait ocean-cli missions run "Which of these two plans suits a 5-person team?" --sync ``` ## ocean-cli missions status ``` Usage: ocean-cli missions status [options] <id> Mission status (and any pending question or approval). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli missions status <id> ``` ## ocean-cli missions poll ``` Usage: ocean-cli missions poll [options] <id> Wait for a mission, then save and print its result (default ./ocean-missions/<id>.json). Options: --timeout <seconds> Max wait time in seconds (default: 600) --poll-interval <seconds> Seconds between status checks (default: 5) --json Output as JSON -o, --output <path> The result file (default ./ocean-missions/<id>.json) --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli missions poll <id> -o mission.json ``` ## ocean-cli missions result ``` Usage: ocean-cli missions result [options] <id> The mission result (no file written unless -o). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli missions result <id> --json ``` ## ocean-cli schedules ``` Usage: ocean-cli schedules [options] [command] Scheduled jobs (cron): agents, queries, workflows, heartbeats. Options: -h, --help display help for command Commands: stats [options] Job counts (active, paused). list [options] List jobs. get [options] <job_id> One job including delivery targets. runs [options] [job_id] Run history (all jobs, or one). pause [options] <job_id> Pause the job. resume [options] <job_id> Resume the job. run-now [options] <job_id> Run the job now and wait for the outcome (prints the execution id). delete [options] <job_id> Delete a job (soft delete; run history is kept). create [options] <name> Create a scheduled job. update [options] <job_id> Update a job. Delivery targets are kept unless --deliver is passed (which replaces them). wake [options] <job_id> Fire the schedule's saved workflow once, now; the schedule itself is untouched. triggers What starts or watches a schedule's runs: inbound webhook and email triggers, outbound signed hooks. ``` ## ocean-cli schedules stats ``` Usage: ocean-cli schedules stats [options] Job counts (active, paused). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules stats ``` ## ocean-cli schedules list ``` Usage: ocean-cli schedules list [options] List jobs. Options: --limit <n> Max rows (1-100) (default: 50) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules list --json ``` ## ocean-cli schedules get ``` Usage: ocean-cli schedules get [options] <job_id> One job including delivery targets. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules get <job_id> ``` ## ocean-cli schedules runs ``` Usage: ocean-cli schedules runs [options] [job_id] Run history (all jobs, or one). Options: --limit <n> Max rows (1-100) (default: 20) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules runs <job_id> --limit 5 ``` ## ocean-cli schedules pause ``` Usage: ocean-cli schedules pause [options] <job_id> Pause the job. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules pause <job_id> ``` ## ocean-cli schedules resume ``` Usage: ocean-cli schedules resume [options] <job_id> Resume the job. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules resume <job_id> ``` ## ocean-cli schedules run-now ``` Usage: ocean-cli schedules run-now [options] <job_id> Run the job now and wait for the outcome (prints the execution id). Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules run-now <job_id> ``` ## ocean-cli schedules delete ``` Usage: ocean-cli schedules delete [options] <job_id> Delete a job (soft delete; run history is kept). Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules delete <job_id> --yes ``` ## ocean-cli schedules create ``` Usage: ocean-cli schedules create [options] <name> Create a scheduled job. Options: --type <type> Job type (choices: "agent", "query", "workflow", "heartbeat") --cron <expr> Cron expression, or @once together with --at --target <id> Agent id (agent), {"agent","query"} JSON (query) or workflow id (workflow) --prompt <text> What the agent does each run --timezone <iana> IANA timezone the cron is read in, e.g. Europe/Berlin (default UTC) --deliver <channel:to> Delivery target, repeatable: telegram:<chat> whatsapp:<num> discord:<chat> slack:<C… or handle> teams:<chat> email:<addr> inapp (default: []) --mission-ref <id> Pre-built agent card id (schedules a harness wrapper) --check-agent <id> heartbeat: agent to check --condition <prompt> heartbeat: condition prompt --trigger <workflow_id> heartbeat: workflow to trigger --notify <channel> heartbeat: notify channel (choices: "telegram", "whatsapp", "discord", "slack", "teams", "email") --notify-to <target> heartbeat: notify target --description <text> Description --at <iso> With --cron @once: when to run, an ISO-8601 time with a UTC offset in the future (workflow or heartbeat jobs) --ends-at <iso> The schedule stops after this ISO-8601 time or ms epoch (workflow or heartbeat jobs) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli schedules create "Weekday brief" --type agent --target research-agent --prompt "top AI news, 5 bullets" --cron "0 9 * * 1-5" --deliver slack:C0123 --deliver email:me@example.com ocean-cli schedules create "Nightly run" --type workflow --target <workflow_id> --cron "0 2 * * *" --dry-run ocean-cli schedules create "Reminder" --type workflow --target <workflow_id> --cron @once --at 2026-10-06T09:00:00Z ocean-cli schedules create "Campaign sweep" --type workflow --target <workflow_id> --cron "0 * * * *" --ends-at 2026-11-01T00:00:00Z ``` ## ocean-cli schedules update ``` Usage: ocean-cli schedules update [options] <job_id> Update a job. Delivery targets are kept unless --deliver is passed (which replaces them). Options: --cron <expr> New schedule --timezone <iana> IANA timezone the cron is read in, e.g. Europe/Berlin --name <text> New name --description <text> New description --prompt <text> New prompt --deliver <channel:to> Replace the delivery targets, repeatable: telegram:<chat> whatsapp:<num> discord:<chat> slack:<C… or handle> teams:<chat> email:<addr> inapp (default: []) --status <status> active (resume) or paused (pause) (choices: "active", "paused") --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli schedules update <job_id> --cron "0 8 * * 1-5" --timezone America/Los_Angeles ocean-cli schedules update <job_id> --prompt "top AI news, 3 bullets" --status active ``` ## ocean-cli schedules wake ``` Usage: ocean-cli schedules wake [options] <job_id> Fire the schedule's saved workflow once, now; the schedule itself is untouched. Options: --note <text> Why this run exists; it becomes the run's input --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules wake <job_id> --note "the customer just replied" ``` ## ocean-cli schedules triggers ``` Usage: ocean-cli schedules triggers [options] [command] What starts or watches a schedule's runs: inbound webhook and email triggers, outbound signed hooks. Options: -h, --help display help for command Commands: webhook A schedule's inbound webhooks: mint, list, rotate, delete. email A schedule's inbound email addresses: mint, list, rotate, delete. hooks A schedule's outbound lifecycle hooks: mint, list, delete. ``` ## ocean-cli schedules triggers webhook ``` Usage: ocean-cli schedules triggers webhook [options] [command] A schedule's inbound webhooks: mint, list, rotate, delete. Options: -h, --help display help for command Commands: mint [options] <job_id> Mint an inbound webhook; a secret is shown once. list [options] <job_id> List the schedule's inbound webhooks (secrets are never listed). rotate [options] <job_id> <token> Rotate an inbound webhook: a successor with the same settings (secret shown once), then the old token is revoked. delete [options] <job_id> <token> Revoke an inbound webhook. ``` ## ocean-cli schedules triggers webhook mint ``` Usage: ocean-cli schedules triggers webhook mint [options] <job_id> Mint an inbound webhook; a secret is shown once. Options: --prompt-template <text> Text for the run input; {dotted.path} fills in a field of the JSON payload, e.g. {pull_request.title} --scheme <scheme> Signature scheme (default generic) (choices: "generic", "github", "gitlab", "standard-webhooks") --filters <json> {"all":[…],"any":[…]} rules on the payload or headers (inline JSON, "-" for stdin, @FILE) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers webhook mint <job_id> --scheme github --prompt-template "PR: {pull_request.title}" ``` ## ocean-cli schedules triggers webhook list ``` Usage: ocean-cli schedules triggers webhook list [options] <job_id> List the schedule's inbound webhooks (secrets are never listed). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers webhook list <job_id> ``` ## ocean-cli schedules triggers webhook rotate ``` Usage: ocean-cli schedules triggers webhook rotate [options] <job_id> <token> Rotate an inbound webhook: a successor with the same settings (secret shown once), then the old token is revoked. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers webhook rotate <job_id> <token> -o new-webhook.json ``` ## ocean-cli schedules triggers webhook delete ``` Usage: ocean-cli schedules triggers webhook delete [options] <job_id> <token> Revoke an inbound webhook. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers webhook delete <job_id> <token> --yes ``` ## ocean-cli schedules triggers email ``` Usage: ocean-cli schedules triggers email [options] [command] A schedule's inbound email addresses: mint, list, rotate, delete. Options: -h, --help display help for command Commands: mint [options] <job_id> Mint an inbound email address; a secret is shown once. list [options] <job_id> List the schedule's inbound email addresses (secrets are never listed). rotate [options] <job_id> <token> Rotate an inbound email address: a successor with the same settings (secret shown once), then the old token is revoked. delete [options] <job_id> <token> Revoke an inbound email address. ``` ## ocean-cli schedules triggers email mint ``` Usage: ocean-cli schedules triggers email mint [options] <job_id> Mint an inbound email address; a secret is shown once. Options: --allowed-from <list> Comma-separated sender addresses or domains (default: anyone) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers email mint <job_id> --allowed-from alice@example.com,example.org ``` ## ocean-cli schedules triggers email list ``` Usage: ocean-cli schedules triggers email list [options] <job_id> List the schedule's inbound email addresses (secrets are never listed). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers email list <job_id> ``` ## ocean-cli schedules triggers email rotate ``` Usage: ocean-cli schedules triggers email rotate [options] <job_id> <token> Rotate an inbound email address: a successor with the same settings (secret shown once), then the old token is revoked. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers email rotate <job_id> <token> -o new-email.json ``` ## ocean-cli schedules triggers email delete ``` Usage: ocean-cli schedules triggers email delete [options] <job_id> <token> Revoke an inbound email address. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers email delete <job_id> <token> --yes ``` ## ocean-cli schedules triggers hooks ``` Usage: ocean-cli schedules triggers hooks [options] [command] A schedule's outbound lifecycle hooks: mint, list, delete. Options: -h, --help display help for command Commands: mint [options] <job_id> Mint an outbound lifecycle hook; a secret is shown once. list [options] <job_id> List the schedule's outbound lifecycle hooks (secrets are never listed). delete [options] <job_id> <id> Revoke an outbound lifecycle hook. ``` ## ocean-cli schedules triggers hooks mint ``` Usage: ocean-cli schedules triggers hooks mint [options] <job_id> Mint an outbound lifecycle hook; a secret is shown once. Options: --url <https-url> https URL that receives the signed run events --events <list> run.started, run.finished, run.failed, run.needs_approval (comma-separated; default: all) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers hooks mint <job_id> --url https://example.com/hook --events run.finished,run.failed ``` ## ocean-cli schedules triggers hooks list ``` Usage: ocean-cli schedules triggers hooks list [options] <job_id> List the schedule's outbound lifecycle hooks (secrets are never listed). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers hooks list <job_id> ``` ## ocean-cli schedules triggers hooks delete ``` Usage: ocean-cli schedules triggers hooks delete [options] <job_id> <id> Revoke an outbound lifecycle hook. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli schedules triggers hooks delete <job_id> <id> --yes ``` ## ocean-cli memory ``` Usage: ocean-cli memory [options] [command] Long-term platform memory: recall, remember, forget, past runs. Options: -h, --help display help for command Commands: recall [options] [query] Read memory. The platform currently returns the whole memory; QUERY is advisory. remember [options] Edit memory: --add TEXT | --replace OLD NEW | --remove TEXT. forget [options] Forget one entry by id (--id), or purge every entry containing some text (--text). Destructive: --yes. search-runs [options] <query> Search past run results ("-" reads the query from stdin). get-run [options] <run_id> One remembered run. ``` ## ocean-cli memory recall ``` Usage: ocean-cli memory recall [options] [query] Read memory. The platform currently returns the whole memory; QUERY is advisory. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli memory recall "pricing" ``` ## ocean-cli memory remember ``` Usage: ocean-cli memory remember [options] Edit memory: --add TEXT | --replace OLD NEW | --remove TEXT. Options: --add <text> Append an entry --replace <values...> Replace the entry containing OLD with NEW (exactly two values) --remove <text> Remove the entry containing TEXT --target <store> Which store (choices: "user", "memory", default: "user") --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli memory remember --add "We ship on Fridays" ocean-cli memory remember --replace "Fridays" "Thursdays" ocean-cli memory remember --remove "Thursdays" --target memory ``` ## ocean-cli memory forget ``` Usage: ocean-cli memory forget [options] Forget one entry by id (--id), or purge every entry containing some text (--text). Destructive: --yes. Options: --id <id> The entry id --text <text> Every entry containing this text --yes Confirm --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli memory forget --id <entry_id> --yes ocean-cli memory forget --text "Fridays" --yes ``` ## ocean-cli memory search-runs ``` Usage: ocean-cli memory search-runs [options] <query> Search past run results ("-" reads the query from stdin). Options: --limit <n> Max rows (1-100) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli memory search-runs "adyen" --limit 5 ``` ## ocean-cli memory get-run ``` Usage: ocean-cli memory get-run [options] <run_id> One remembered run. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli memory get-run <run_id> ``` ## ocean-cli learn ``` Usage: ocean-cli learn [options] [command] Distil a URL or text into a private skill. Options: -h, --help display help for command Commands: add [options] <source> Learn from a URL or literal text (waits for the distillation). list [options] List learned skills. forget [options] <name> Delete a learned skill. ``` ## ocean-cli learn add ``` Usage: ocean-cli learn add [options] <source> Learn from a URL or literal text (waits for the distillation). Options: --name <id> Kebab-case skill id --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli learn add https://docs.example.com/guide --name example-guide ``` ## ocean-cli learn list ``` Usage: ocean-cli learn list [options] List learned skills. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli learn list ``` ## ocean-cli learn forget ``` Usage: ocean-cli learn forget [options] <name> Delete a learned skill. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli learn forget example-guide --yes ``` ## ocean-cli templates ``` Usage: ocean-cli templates [options] [command] Workflow templates. Options: -h, --help display help for command Commands: list [options] List templates. get [options] <template_id> One template (with its YAML). use [options] <template_id> Instantiate a template as a saved workflow. ``` ## ocean-cli templates list ``` Usage: ocean-cli templates list [options] List templates. Options: --category <name> Category --limit <n> Max rows (1-100) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli templates list --category sales ``` ## ocean-cli templates get ``` Usage: ocean-cli templates get [options] <template_id> One template (with its YAML). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli templates get <id> ``` ## ocean-cli templates use ``` Usage: ocean-cli templates use [options] <template_id> Instantiate a template as a saved workflow. Options: --name <text> Workflow name --folder <name> Folder --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli templates use <id> --name "My flow" --folder Sales ``` ## ocean-cli chat ``` Usage: ocean-cli chat [options] [command] One-shot platform chat turns (non-interactive; use conversation ids to continue). Options: -h, --help display help for command Commands: send [options] <message> Send one message; the reply is printed verbatim. new [options] Start a new conversation (prints its id). sessions [options] List conversations. history [options] <conversation_id> Messages of one conversation. ``` ## ocean-cli chat send ``` Usage: ocean-cli chat send [options] <message> Send one message; the reply is printed verbatim. Options: --conversation-id <id> Continue this conversation --no-context Do not send your time zone and locale (user_context) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli chat send "show my last three runs" echo "and export the first one as docx" | ocean-cli chat send - --conversation-id <id> ``` ## ocean-cli chat new ``` Usage: ocean-cli chat new [options] Start a new conversation (prints its id). Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli chat new --json ``` ## ocean-cli chat sessions ``` Usage: ocean-cli chat sessions [options] List conversations. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli chat sessions ``` ## ocean-cli chat history ``` Usage: ocean-cli chat history [options] <conversation_id> Messages of one conversation. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli chat history <id> ``` ## ocean-cli discovery ``` Usage: ocean-cli discovery [options] [command] The registry: search cards, get recommendations, resolve a task to a tool, read a card. Options: -h, --help display help for command Commands: search [options] [q] Search registry cards. recommend [options] <task> The best card for a task, with a score. resolve [options] <task> Resolve a task to one tool, its operation and an example call, with a calibrated confidence. card [options] <type> <id> One card as markdown with YAML frontmatter. ``` ## ocean-cli discovery search ``` Usage: ocean-cli discovery search [options] [q] Search registry cards. Options: --kind <kind> Card kind (choices: "agents", "skills", "plugins", "mcp-servers", "connectors", "models", "providers", "workflows", "experts", "backgrounds", "personas", "digital-twins") --type <type> Singular type filter (agent, skill, …) --category <name> Category --status <status> Status (default active) --tier <tier> Tier (choices: "verified", "community") --mode <mode> Search mode (choices: "hybrid", "keyword", "vector") --fields <fields> Field projection per card (choices: "list", "summary", "commands", "full") --limit <n> Page size (max 100) (default: 20) --offset <n> Page offset (default: 0) --quality Include quality scores --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli discovery search "web search" --kind agents ocean-cli discovery search --kind skills --limit 50 --json ``` ## ocean-cli discovery recommend ``` Usage: ocean-cli discovery recommend [options] <task> The best card for a task, with a score. Options: --type <type> Card type to recommend (plural accepted) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli discovery recommend "summarise a PDF" --type agents ``` ## ocean-cli discovery resolve ``` Usage: ocean-cli discovery resolve [options] <task> Resolve a task to one tool, its operation and an example call, with a calibrated confidence. Options: --kinds <list> Card kinds to consider (comma-separated) --category <name> Category --status <status> Status --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli discovery resolve "search the web for a company" --kinds agents,tools ``` ## ocean-cli discovery card ``` Usage: ocean-cli discovery card [options] <type> <id> One card as markdown with YAML frontmatter. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli discovery card agent one-agent ``` ## ocean-cli models ``` Usage: ocean-cli models [options] [command] The models plane (your ozk_ key): catalog, chat and text completions, embeddings, generation cost, credits, key, endpoints. Options: -h, --help display help for command Commands: list [options] List models with context, pricing per 1M tokens and capabilities. pricing [options] The public price list (no key): list price, Ocean price and discount per model, USD per 1M tokens. chat [options] <prompt> One chat completion. embed [options] <text> Create an embedding (openai, mistral, google and cohere models). complete [options] <prompt> Legacy text completion: a chat completion with one user message, answered in the completions shape. generation [options] <id> Post-hoc usage and cost of one completion (its gen-… id or x-request-id). credits [options] Lifetime credits bought and used by the calling account, in USD. key [options] The calling key: label, monthly limit, usage this month, rate limit. endpoints [options] <model> The endpoint that serves one model (author/slug), with per-token pricing. ``` ## ocean-cli models list ``` Usage: ocean-cli models list [options] List models with context, pricing per 1M tokens and capabilities. Options: --compat Call the /api/v1 (OpenRouter-compatible) path instead of /v1 --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli models list --json ``` ## ocean-cli models pricing ``` Usage: ocean-cli models pricing [options] The public price list (no key): list price, Ocean price and discount per model, USD per 1M tokens. Options: --compat Call the /api/v1 (OpenRouter-compatible) path instead of /v1 --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli models pricing --json ``` ## ocean-cli models chat ``` Usage: ocean-cli models chat [options] <prompt> One chat completion. Options: --model <id> Model id (e.g. openai/gpt-5, @preset/fast) --system <text> System prompt --max-tokens <n> Max output tokens --temperature <n> Sampling temperature (0-2) --stream Stream tokens (with --json: one chunk per line) --compat Call the /api/v1 (OpenRouter-compatible) path instead of /v1 --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file (with --stream: the assembled content) --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli models chat "hello" --model openai/gpt-5 echo "prompt" | ocean-cli models chat - --model anthropic/claude-sonnet-5 --stream ``` ## ocean-cli models embed ``` Usage: ocean-cli models embed [options] <text> Create an embedding (openai, mistral, google and cohere models). Options: --model <id> Embedding model id --compat Call the /api/v1 (OpenRouter-compatible) path instead of /v1 --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli models embed "hello world" --model openai/text-embedding-3-small --json ``` ## ocean-cli models complete ``` Usage: ocean-cli models complete [options] <prompt> Legacy text completion: a chat completion with one user message, answered in the completions shape. Options: --model <id> Model id --max-tokens <n> Max output tokens --compat Call the /api/v1 (OpenRouter-compatible) path instead of /v1 --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli models complete "Say ok" --model openai/gpt-5 --max-tokens 5 ``` ## ocean-cli models generation ``` Usage: ocean-cli models generation [options] <id> Post-hoc usage and cost of one completion (its gen-… id or x-request-id). Options: --compat Call the /api/v1 (OpenRouter-compatible) path instead of /v1 --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli models generation gen-abc123 --json ``` ## ocean-cli models credits ``` Usage: ocean-cli models credits [options] Lifetime credits bought and used by the calling account, in USD. Options: --compat Call the /api/v1 (OpenRouter-compatible) path instead of /v1 --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli models credits ``` ## ocean-cli models key ``` Usage: ocean-cli models key [options] The calling key: label, monthly limit, usage this month, rate limit. Options: --compat Call the /api/v1 (OpenRouter-compatible) path instead of /v1 --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli models key --json ``` ## ocean-cli models endpoints ``` Usage: ocean-cli models endpoints [options] <model> The endpoint that serves one model (author/slug), with per-token pricing. Options: --compat Call the /api/v1 (OpenRouter-compatible) path instead of /v1 --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli models endpoints openai/gpt-5 --json ``` ## ocean-cli mcp ``` Usage: ocean-cli mcp [options] [command] MCP: the platform tool list, and outbound runs against connected MCP servers. Options: -h, --help display help for command Commands: tools [options] List the platform MCP tools. runs Outbound MCP runs: connect servers, list tools, call one, close. ``` ## ocean-cli mcp tools ``` Usage: ocean-cli mcp tools [options] List the platform MCP tools. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli mcp tools ``` ## ocean-cli mcp runs ``` Usage: ocean-cli mcp runs [options] [command] Outbound MCP runs: connect servers, list tools, call one, close. Options: -h, --help display help for command Commands: create [options] Open a run against connected servers. tools [options] <run_id> Tools available in a run. call [options] <run_id> <name> Call one tool (name is mcp__<server>__<tool>). delete [options] <run_id> Close a run (releases the connections; no data is deleted). ``` ## ocean-cli mcp runs create ``` Usage: ocean-cli mcp runs create [options] Open a run against connected servers. Options: --server <id> Server id (repeatable) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli mcp runs create --server github --server slack --json ``` ## ocean-cli mcp runs tools ``` Usage: ocean-cli mcp runs tools [options] <run_id> Tools available in a run. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli mcp runs tools <run_id> ``` ## ocean-cli mcp runs call ``` Usage: ocean-cli mcp runs call [options] <run_id> <name> Call one tool (name is mcp__<server>__<tool>). Options: --args <json> Arguments as a JSON object (inline, - or @file) --timeout-ms <n> Per-call timeout --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli mcp runs call <run_id> mcp__github__search --args '{"q":"ocean"}' ``` ## ocean-cli mcp runs delete ``` Usage: ocean-cli mcp runs delete [options] <run_id> Close a run (releases the connections; no data is deleted). Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli mcp runs delete <run_id> --yes ``` ## ocean-cli traces ``` Usage: ocean-cli traces [options] [command] Execution traces: spans and gateway requests by trace id. Options: -h, --help display help for command Commands: get [options] <trace_id> Spans and gateway requests of one trace (an unknown id answers 404 not_found). ``` ## ocean-cli traces get ``` Usage: ocean-cli traces get [options] <trace_id> Spans and gateway requests of one trace (an unknown id answers 404 not_found). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli traces get <execution_id> ``` ## ocean-cli keys ``` Usage: ocean-cli keys [options] [command] One API keys (ozk_) — needs a Studio session. Options: -h, --help display help for command Commands: list [options] List your API keys (redacted). create [options] <name> Create a key; the secret is shown once. revoke [options] <key_id> Revoke a key. ``` ## ocean-cli keys list ``` Usage: ocean-cli keys list [options] List your API keys (redacted). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli keys list ``` ## ocean-cli keys create ``` Usage: ocean-cli keys create [options] <name> Create a key; the secret is shown once. Options: --expires-in-days <n> One of 7, 30, 90, 180, 365 (default: never) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli keys create ci --expires-in-days 90 ``` ## ocean-cli keys revoke ``` Usage: ocean-cli keys revoke [options] <key_id> Revoke a key. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli keys revoke <key_id> --yes ``` ## ocean-cli balance ``` Usage: ocean-cli balance [options] Prepaid credit balance (needs a Studio session). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli balance --json ``` ## ocean-cli usage ``` Usage: ocean-cli usage [options] Usage and cost rollups (needs a Studio session). Options: --by <dim> Group by (default: user) (choices: "user", "workflow", "model", "channel") --range <range> Range (default: 30d) (choices: "7d", "30d", "90d") --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli usage --by model --range 30d ``` ## ocean-cli nodes ``` Usage: ocean-cli nodes [options] [command] Local nodes (device daemons) — needs a Studio session. Options: -h, --help display help for command Commands: list [options] List your nodes, or a team fleet. get [options] <node_id> One of your nodes (metrics in --json). update-mode [options] <node_id> <mode> Daemon update mode: auto, off, or pin (needs --version). A connected node installs the target version in this call. command [options] <node_id> <command> Send ping or restart over the live socket. network-role [options] <node_id> <role> master or member within the node’s network. revoke [options] <node_id> Block this node and close its socket. delete [options] <node_id> Forget an offline node record. ``` ## ocean-cli nodes list ``` Usage: ocean-cli nodes list [options] List your nodes, or a team fleet. Options: --scope <scope> team = a team's fleet (owner/admin) (choices: "team") --team-id <id> Team id with --scope team --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli nodes list ocean-cli nodes list --scope team --team-id <id> ``` ## ocean-cli nodes get ``` Usage: ocean-cli nodes get [options] <node_id> One of your nodes (metrics in --json). Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli nodes get <node_id> --json ``` ## ocean-cli nodes update-mode ``` Usage: ocean-cli nodes update-mode [options] <node_id> <mode> Daemon update mode: auto, off, or pin (needs --version). A connected node installs the target version in this call. Arguments: node_id mode auto | off | pin (choices: "auto", "off", "pin") Options: --version <x.y.z> The published version to pin --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli nodes update-mode <node_id> auto ocean-cli nodes update-mode <node_id> pin --version 1.3.0 ``` ## ocean-cli nodes command ``` Usage: ocean-cli nodes command [options] <node_id> <command> Send ping or restart over the live socket. Arguments: node_id command ping | restart (choices: "ping", "restart") Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli nodes command <node_id> ping ``` ## ocean-cli nodes network-role ``` Usage: ocean-cli nodes network-role [options] <node_id> <role> master or member within the node’s network. Arguments: node_id role master | member (choices: "master", "member") Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli nodes network-role <node_id> master ``` ## ocean-cli nodes revoke ``` Usage: ocean-cli nodes revoke [options] <node_id> Block this node and close its socket. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli nodes revoke <node_id> --yes ``` ## ocean-cli nodes delete ``` Usage: ocean-cli nodes delete [options] <node_id> Forget an offline node record. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli nodes delete <node_id> --yes ``` ## ocean-cli campaigns ``` Usage: ocean-cli campaigns [options] [command] Campaign ledgers on a workflow — needs a Studio session. Options: -h, --help display help for command Commands: status [options] <workflow_id> Ledger status, counts and cost of the campaign. setup [options] <workflow_id> Arm the campaign ledger on a workflow from a campaign config. complete [options] <workflow_id> Complete a human task row of the campaign. resume [options] <workflow_id> Resume the campaign. pause [options] <workflow_id> Pause the campaign. stop [options] <workflow_id> Stop the campaign and release its leases. ``` ## ocean-cli campaigns status ``` Usage: ocean-cli campaigns status [options] <workflow_id> Ledger status, counts and cost of the campaign. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli campaigns status <workflow_id> ``` ## ocean-cli campaigns setup ``` Usage: ocean-cli campaigns setup [options] <workflow_id> Arm the campaign ledger on a workflow from a campaign config. Options: --config <json> The campaign config: inline JSON, "-" for stdin, or @FILE --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli campaigns setup <workflow_id> --config @campaign.json ``` ## ocean-cli campaigns complete ``` Usage: ocean-cli campaigns complete [options] <workflow_id> Complete a human task row of the campaign. Options: --item <key> Ledger item key of the human task --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli campaigns complete <workflow_id> --item <key> ``` ## ocean-cli campaigns resume ``` Usage: ocean-cli campaigns resume [options] <workflow_id> Resume the campaign. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli campaigns resume <workflow_id> ``` ## ocean-cli campaigns pause ``` Usage: ocean-cli campaigns pause [options] <workflow_id> Pause the campaign. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli campaigns pause <workflow_id> ``` ## ocean-cli campaigns stop ``` Usage: ocean-cli campaigns stop [options] <workflow_id> Stop the campaign and release its leases. Options: --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli campaigns stop <workflow_id> ``` ## ocean-cli connections ``` Usage: ocean-cli connections [options] [command] Provider connections (API keys, OAuth) — needs a Studio session; `connections api …` is the same door over the API key. Options: -h, --help display help for command Commands: api The same connections over the API key (/v1/connections): list, add, remove, link — yours or a customer’s (--on-behalf-of). list [options] Your connections. providers [options] Providers that can be connected. get [options] <provider> One connection. add-key [options] <provider> Connect a provider with an API key (probed live; a rejected key is a 400). The key comes from --key - (stdin), --key FILE, or a hidden prompt — never from argv. remove [options] <provider> Disconnect a provider. ``` ## ocean-cli connections api ``` Usage: ocean-cli connections api [options] [command] The same connections over the API key (/v1/connections): list, add, remove, link — yours or a customer’s (--on-behalf-of). Options: -h, --help display help for command Commands: list [options] Your connections, or one customer’s (status only). add [options] <provider> Bring a key for a provider (probed live where the provider has a probe). The key comes from --key - (stdin), --key FILE, or a hidden prompt — never from argv. remove [options] <provider> Disconnect a provider (yours, or your customer’s). link [options] <provider> A hosted connect link to send to your customer (works once, 15 minutes, no Studio login). ``` ## ocean-cli connections api list ``` Usage: ocean-cli connections api list [options] Your connections, or one customer’s (status only). Options: --on-behalf-of <subject> Your customer’s id (the connection is theirs) --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli connections api list --on-behalf-of cust-7 ``` ## ocean-cli connections api add ``` Usage: ocean-cli connections api add [options] <provider> Bring a key for a provider (probed live where the provider has a probe). The key comes from --key - (stdin), --key FILE, or a hidden prompt — never from argv. Options: --on-behalf-of <subject> Your customer’s id (the connection is theirs) --key <source> "-" for stdin or a file path; omitted = hidden prompt --secret <source> An API secret for providers that take one: "-" or a file path --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: printf '%s' "$TAVILY_KEY" | ocean-cli connections api add tavily --key - ``` ## ocean-cli connections api remove ``` Usage: ocean-cli connections api remove [options] <provider> Disconnect a provider (yours, or your customer’s). Options: --on-behalf-of <subject> Your customer’s id (the connection is theirs) --yes Confirm --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli connections api remove tavily --yes ``` ## ocean-cli connections api link ``` Usage: ocean-cli connections api link [options] <provider> A hosted connect link to send to your customer (works once, 15 minutes, no Studio login). Options: --on-behalf-of <subject> Your customer’s id (the connection is theirs) --return-url <https-url> Where the customer lands afterwards (?connected=<provider>) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli connections api link gmail --on-behalf-of cust-7 --return-url https://example.com/done ``` ## ocean-cli connections list ``` Usage: ocean-cli connections list [options] Your connections. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli connections list ``` ## ocean-cli connections providers ``` Usage: ocean-cli connections providers [options] Providers that can be connected. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli connections providers --json ``` ## ocean-cli connections get ``` Usage: ocean-cli connections get [options] <provider> One connection. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli connections get tavily ``` ## ocean-cli connections add-key ``` Usage: ocean-cli connections add-key [options] <provider> Connect a provider with an API key (probed live; a rejected key is a 400). The key comes from --key - (stdin), --key FILE, or a hidden prompt — never from argv. Options: --key <source> "-" for stdin or a file path; omitted = hidden prompt --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: printf '%s' "$TAVILY_KEY" | ocean-cli connections add-key tavily --key - ``` ## ocean-cli connections remove ``` Usage: ocean-cli connections remove [options] <provider> Disconnect a provider. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli connections remove tavily --yes ``` ## ocean-cli secrets ``` Usage: ocean-cli secrets [options] [command] Developer secrets (encrypted at rest, scoped to a company, team or project) — needs a Studio session. Options: -h, --help display help for command Commands: list [options] List secret names in a scope (never values). set [options] <name> Create a secret. The value comes from --value - (stdin), --value FILE, or a hidden prompt — never from argv. delete [options] <id> Delete a secret. audit [options] Secret access audit log of a scope. ``` ## ocean-cli secrets list ``` Usage: ocean-cli secrets list [options] List secret names in a scope (never values). Options: --team-id <id> Team scope --project-id <id> Project scope --company-id <id> Company scope --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli secrets list --team-id <id> ``` ## ocean-cli secrets set ``` Usage: ocean-cli secrets set [options] <name> Create a secret. The value comes from --value - (stdin), --value FILE, or a hidden prompt — never from argv. Options: --value <source> "-" for stdin or a file path; omitted = hidden prompt --description <text> Description --dry-run Show what would be sent without making API calls --team-id <id> Team scope --project-id <id> Project scope --company-id <id> Company scope --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: printf '%s' "$VALUE" | ocean-cli secrets set MY_KEY --value - --team-id <id> ``` ## ocean-cli secrets delete ``` Usage: ocean-cli secrets delete [options] <id> Delete a secret. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli secrets delete <id> --yes ``` ## ocean-cli secrets audit ``` Usage: ocean-cli secrets audit [options] Secret access audit log of a scope. Options: --team-id <id> Team scope --project-id <id> Project scope --company-id <id> Company scope --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli secrets audit --team-id <id> --json ``` ## ocean-cli documents ``` Usage: ocean-cli documents [options] [command] Stored documents — needs a Studio session. Options: -h, --help display help for command Commands: list [options] List documents. get [options] <id> Document metadata. download [options] <id> Download the file (default name: the document's filename). delete [options] <id> Delete a document. ``` ## ocean-cli documents list ``` Usage: ocean-cli documents list [options] List documents. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli documents list ``` ## ocean-cli documents get ``` Usage: ocean-cli documents get [options] <id> Document metadata. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli documents get <id> ``` ## ocean-cli documents download ``` Usage: ocean-cli documents download [options] <id> Download the file (default name: the document's filename). Options: --json Output as JSON -o, --output <path> The file to write (default: the document's filename here) --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli documents download <id> ocean-cli documents download <id> -o report.pdf --force ``` ## ocean-cli documents delete ``` Usage: ocean-cli documents delete [options] <id> Delete a document. Options: --yes Confirm --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli documents delete <id> --yes ``` ## ocean-cli teams ``` Usage: ocean-cli teams [options] [command] Teams — needs a Studio session. Options: -h, --help display help for command Commands: list [options] Your teams. get [options] <id> One team. members [options] <id> Team members. ``` ## ocean-cli teams list ``` Usage: ocean-cli teams list [options] Your teams. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli teams list ``` ## ocean-cli teams get ``` Usage: ocean-cli teams get [options] <id> One team. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli teams get <id> ``` ## ocean-cli teams members ``` Usage: ocean-cli teams members [options] <id> Team members. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli teams members <id> ``` ## ocean-cli projects ``` Usage: ocean-cli projects [options] [command] Projects — needs a Studio session. Options: -h, --help display help for command Commands: list [options] Your projects. get [options] <id> One project. ``` ## ocean-cli projects list ``` Usage: ocean-cli projects list [options] Your projects. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli projects list ``` ## ocean-cli projects get ``` Usage: ocean-cli projects get [options] <id> One project. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Example: ocean-cli projects get <id> ``` ## ocean-cli skills ``` Usage: ocean-cli skills [options] [command] Install, update and manage PlungeAI agent skills from skills.plungeai.com. Options: -h, --help display help for command Commands: list [options] List available skills. install [options] Install or update skills. --skill adds just those to the managed set; a full install also removes managed skills that left the catalog. Unmanaged skills are never touched. uninstall [options] Remove the skills this CLI installed (listed in .ocean-cli-skills.json); unmanaged skills stay. ``` ## ocean-cli skills list ``` Usage: ocean-cli skills list [options] List available skills. Options: --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli skills list ocean-cli skills list --json ``` ## ocean-cli skills install ``` Usage: ocean-cli skills install [options] Install or update skills. --skill adds just those to the managed set; a full install also removes managed skills that left the catalog. Unmanaged skills are never touched. Options: --project Install into the project root (.agents/skills, plus .claude/skills when present) instead of globally --skill <name> Skill to install or update, added to the managed set (repeatable); default: the whole catalog (default: []) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli skills install ocean-cli skills install --project --skill plungeai-workflows --skill plungeai-in-cursor ocean-cli skills install --dry-run ``` ## ocean-cli skills uninstall ``` Usage: ocean-cli skills uninstall [options] Remove the skills this CLI installed (listed in .ocean-cli-skills.json); unmanaged skills stay. Options: --project Project targets instead of global --skill <name> Managed skill to remove (repeatable); default: all managed (default: []) --dry-run Show what would be sent without making API calls --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli skills uninstall ocean-cli skills uninstall --project --skill plungeai-in-cursor --dry-run ``` ## ocean-cli completion ``` Usage: ocean-cli completion [options] [command] Shell completion for bash, zsh and fish. Options: -h, --help display help for command Commands: show [options] Print the completion script to stdout (--json: {shell, script}). install [options] Append the completion loader to your shell rc file (safe to run twice). ``` ## ocean-cli completion show ``` Usage: ocean-cli completion show [options] Print the completion script to stdout (--json: {shell, script}). Options: --shell <shell> Shell type (auto-detected from $SHELL) (choices: "bash", "zsh", "fish") --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli completion show --shell zsh ocean-cli completion show --shell bash > /usr/local/etc/bash_completion.d/ocean-cli ``` ## ocean-cli completion install ``` Usage: ocean-cli completion install [options] Append the completion loader to your shell rc file (safe to run twice). Options: --shell <shell> Shell type (auto-detected from $SHELL) (choices: "bash", "zsh", "fish") --json Output as JSON -o, --output <path> Also save the JSON result to this file --force Overwrite an existing output file -h, --help display help for command Examples: ocean-cli completion install ocean-cli completion install --shell fish ``` ## ocean-cli update ``` Usage: ocean-cli update [options] Update to the latest published version (npm). Options: --json Output as JSON --check Check for updates without installing --force Reinstall even if already at the latest version --dry-run Show what would be sent without making API calls -h, --help display help for command Examples: ocean-cli update --check ocean-cli update ocean-cli update --dry-run ``` # plungeai-agents Source: https://www.plungeai.com/docs/skills/plungeai-agents Run a PlungeAI registry agent: recognize the two kinds (prompt-driven vs structured tool-agents), list them (`GET /v1/agents`), execute a prompt-driven one sync or async (`plungeai_execute_agent` MCP / `POST /v1/agents/{id}/execute` REST / `ocean-cli agents run`, incl. streaming SSE), and redeem results by execution id (`plungeai_get_result` / `GET /v1/agents/results/{workflowId}/{taskId}`). Use when running a single building-block agent (search, LLM, documents, …), choosing the right execution door for an agent card, polling/redeeming an async run, or fixing a `missing_prompt`/`unknown_agent`/`unknown_model` error. For finding an agent id use `plungeai-discovery`; for structured tool-agents with typed params/operations use `plungeai-tools-connectors`; for chaining several agents use `plungeai-workflows`; for model/provider detail use `plungeai-models`. To run YOUR saved agent or bot by id (`agent: <id>@n` on `POST /v1/execute`, the Agent Call door), see the section below. [Download zip](https://skills.plungeai.com/plungeai-agents.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-agents/SKILL.md) An **agent** is a deployed capability with a uniform interface: it accepts a task, does one job well, and stores its result where the platform can hand it to the next step. Agents are the building blocks workflows chain, missions call, and tools are a structured sub-species of. The catalog is **live and active-only** — execution refuses any id that isn't currently active. ## Prerequisites - Self-service `ozk_` key from **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`). - MCP: `https://mcp.plungeai.com/v1`. REST: `https://api.plungeai.com`. ## Discovery first Never call an agent id from memory. `plungeai_list_agents {search: "<capability in plain words>"}` (MCP) or `GET /v1/discovery/search?kind=agents&q=…` (REST) — full mechanics in `plungeai-discovery`. Fetch the full card (`plungeai_list_agents {agent_id}` / `GET /v1/discovery/cards/agent/{id}`) before first use of an unfamiliar agent — it tells you which of the two kinds below you have, and carries "Not for → use X instead" redirects. ## Two kinds — pick the right execution door | Kind | How you recognize it | How to execute | |---|---|---| | **Prompt-driven** | Card describes free-text input (`one-agent` — the LLM agent, with an optional `persona` — and search agents like `brave-agent`, `exa-agent`) | This skill: `plungeai_execute_agent` / `POST /v1/agents/{id}/execute` with a `prompt` | | **Structured tool-agent** | Card carries a **Parameters table** / operations (document converters, weather, data-table, calendar, payments) | `plungeai-tools-connectors`: fetch the contract, then typed `params` | A third thing is **your own saved agent or bot** (`kind: agent | bot`): not a registry agent, run by id through the Agent Call door (below). Sending prose to a structured agent (or typed fields to a prompt-driven one) is the most common execution mistake — the card tells you which you have. ## Execute — MCP ```json plungeai_execute_agent { agent: "one-agent", prompt: "Summarize the three biggest risks in this text: ..." } ``` Always sync (no `mode` param); `session_id` continues a conversational session; `persona`/`model`/`provider`/`maxTokens`/`temperature`/`top_p`/ `reasoning_effort`/`thinking_level`/`streaming` override per call. Full parameter table, failures, and `plungeai_get_result` — [`references/mcp-execute-and-result.md`](/skills/plungeai-agents/references/mcp-execute-and-result). ## Execute — REST ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"prompt": "One-paragraph brief: the current state of solid-state batteries", "model": "claude-sonnet-5", "maxTokens": 2048}' ``` `sync: false` → 202 pointer, redeemed at `GET /v1/agents/results/{workflowId}/{taskId}` (poll on `404 not_ready`). `stream: true` → an OpenAI-shaped `chat.completion.chunk` SSE instead of a JSON body. Full field list, streaming shape, error catalogue, and the polling pattern — [`references/rest-agents-plane.md`](/skills/plungeai-agents/references/rest-agents-plane). ## Over the One API — the agents plane Same `ozk_` key (`plungeai-api-setup`). The three surfaces side by side: | Job | REST | MCP | CLI `ocean-cli agents …` | |---|---|---|---| | list | `GET /v1/agents?limit=&offset=` (≤ 100) → `{agents: [card + credential], count}`; `GET /v1/agents/categories` → `{categories: [{name, count}], count}` | `plungeai_list_agents` | `list`, `categories` | | run | `POST /v1/agents/{id}/execute {prompt \| input \| messages, persona?, model?, provider?, maxTokens?, temperature?, top_p?, reasoning_effort?, thinking_level?, system?, sync?, stream?, user_context?, format?}` | `plungeai_execute_agent` (always sync) | `run ID PROMPT [--stream]` | | redeem | `GET /v1/agents/results/{workflowId}/{taskId}` → `{content, content_type, workflow_id, task_id}` | `plungeai_get_result` | `result WORKFLOW_ID [TASK_ID]` | What builders get wrong: - **Only `persona` is forwarded** among the capability fields. `skills`, `experts`, `backgrounds`, `plugins` and `mcp` sent here are ignored without an error: put them on a `type: harness` task (`plungeai-skills-plugins`). - **A `404 not_ready` can be forever.** A pointer that is not yours, or never existed, reads `not_ready` exactly like a run still in flight: cap your poll loop (2s+ steps). The terminal answers are `409 execution_failed` (the message is the run's error) and `422 empty_completion` (the model returned no visible content: raise `maxTokens` to 64 or more, or change the model). - **A pinned `model` is checked before the run opens** (`422 unknown_model`; send `provider` next to it, and take the slug from `GET /v1/models` — `plungeai-models`). - **`424 connection_required` carries `error.connect`** (`{provider, method, link}`) for a connection-tier agent; read the card's `credential` first (`plungeai-tools-connectors`). - **MCP `format` is `markdown` or `json`.** The default is markdown for an AI reader to relay verbatim; `format: "json"` on `plungeai_get_result` returns the run's status record plus `result` and `tasks`. REST takes `json`, `yaml`, `markdown` or `text`. - **A replayed `x-trace-id` is `409 duplicate_execution_id`**; send a fresh UUID or none. The `workflow_id` in every answer is the execution id the `/v1/executions` routes take (also the `X-Execution-Id` header on streamed and non-JSON answers). - A paused agent turn replies with an `action_token`; send the same call again with `approved: true` and that token (both, or `400 invalid_params`), `allow_scope` `once` | `run` | `always`. ## Your saved agent or bot by id — the Agent Call door `POST /v1/execute` (level 3 of the One API) takes the program as its body and always runs it through the engine, so a saved bot keeps its stored policy — permission presets, admin locks, spend caps — on this door as on every other: ```bash curl -s -X POST https://api.plungeai.com/v1/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"agent": "<saved id>@3", "input": "Check the pricing page"}' ``` - `agent: <id>` runs the live YAML; `agent: <id>@3` pins snapshot 3 (the answer carries `x-ocean-version: 3`). A building-block id (`agent: brave-agent` + its own fields, or a bare `agent: one-agent` + `prompt`) runs as a one-task workflow; `workflow:` or root `tasks:` runs inline YAML. - Root keys that steer the call: `input`, `inputs`, `format`, `user_context`, `stream` (SSE), `dry_run` (validates and reports `{kind, agent, tasks, warnings}`; no run, no row, no spend) and `on_behalf_of` (run a connection-tier agent on your customer's connected account). - Answers carry `x-ocean-agent`, `x-ocean-tasks`, `x-ocean-version`. `404 agent_not_found` for an id that is not yours; `400 invalid_body` for a malformed ref or a non-boolean flag. - The same bot is `plungeai_execute_workflow {workflow_id}` over MCP and `ocean-cli workflows run <id>` in a terminal. The per-id route `POST /v1/workflows/{id}/execute` is unchanged: the three API levels all stay. Every route and error: `plungeai-api-setup` (`references/bots-and-door.md`); the bot's own keys and policy: `plungeai-bot-agent`. ## Redeem a result later ```json plungeai_get_result {execution_id: "<id from the run's footer>"} ``` Every execution — even a single agent call — hands back a `workflow_id`/`task_id` (or `execution_id`) pair you can redeem later, or read one step's output with `task_id`. ## Gotchas - **"Show me my agents" ≠ the registry.** Users mean their saved workflows (`plungeai_list_workflows`); `plungeai_list_agents` is the platform's own building-block catalog, for when YOU are composing. - **`plungeai_execute_agent` has no async mode.** For a long single-agent run, use the One API with `sync: false`, or wrap it in a one-task workflow via `plungeai_execute_workflow {mode: "async"}` (`plungeai-workflows`). - **Unknown `model` fails BEFORE the run starts** (`needs_input` / `422 unknown_model`), with near-match suggestions — never a silent bad default. - **Idempotency.** Payment-, messaging-, and automation-class agents may duplicate side effects on re-runs. Check execution status before re-firing a call that may already have acted. - **A `502 result_unavailable` on a sync call isn't always a failure** — the result may land late; its error body still carries `workflow_id`/`task_id` to redeem with. ## Related skills - `plungeai-discovery` — search, full cards, tool contracts, templates. - `plungeai-tools-connectors` — structured tool-agents, typed params, connected-account status. - `plungeai-models` — provider factory (`model`/`provider` fields) and the money plane for your own code. - `plungeai-workflows` — chain several agents; async execution surface for long single-agent runs. - `plungeai-results-traces` — tracing a run end-to-end, execution history, cost. ## Reference pages <CardGroup cols={2}> <Card title="Executing agents over MCP — execute_agent, get_result" icon="file-text" href="/skills/plungeai-agents/references/mcp-execute-and-result"> Two execution styles, chosen by the agent's card: </Card> <Card title="Agents — the registry, discovery, and execution" icon="file-text" href="/skills/plungeai-agents/references/registry"> An agent is a deployed capability with a uniform interface: it accepts a task, does one job well (search the web, call an LLM, convert a document, post to… </Card> <Card title="Agents plane — /v1/agents" icon="file-text" href="/skills/plungeai-agents/references/rest-agents-plane"> Execute any active platform agent with a plain prompt. </Card> </CardGroup> # Executing agents over MCP — execute_agent, get_result Source: https://www.plungeai.com/docs/skills/plungeai-agents/references/mcp-execute-and-result <!-- sources-of-truth: orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/server.ts, orchestration/mcp-gateway/tool-exec.ts, orchestration/mcp-gateway/registry-cards.ts, orchestration/mcp-gateway/tool-outcome.ts | last-synced: 2026-09-24 (execute_agent/get_result half of the original discovery-and-execution.md; the list_agents/get_tool_contract half lives in plungeai-discovery, the execute_tool half lives in plungeai-tools-connectors. Re-verified: ExecuteAgentSchema field-by-field, promptText max 65536, against tools.ts — all match, no drift found) --> Two execution styles, chosen by the agent's card: - **Prompt-driven agents** (a prose brief does the work — research, writing, LLM tasks) → `plungeai_execute_agent`. - **Structured tool-agents** (cards with a Parameters table — posting, document conversion, weather, payments) take typed fields, not prose → `plungeai_get_tool_contract` then `plungeai_execute_tool` (see `plungeai-tools-connectors`). When unsure, fetch the card (`plungeai_list_agents {agent_id}`, see `plungeai-discovery`) — a Parameters table means structured. --- ## plungeai_execute_agent **Purpose:** run a single PROMPT-DRIVEN agent once. Pre-built agent cards (capability packs rather than deployed services) are lowered automatically to a harness run — same call shape either way. Always sync (no `mode` param). **Parameters** | Param | Type | Notes | |---|---|---| | `agent` | string, required | Agent id from a live `plungeai_list_agents` search. | | `prompt` | string ≤65536, required | The brief. | | `persona` | string | Persona id to inject. | | `model` | string | Model override — must exist in the platform model catalog (see failure below). Omit to use the agent's default. | | `provider` | string | Provider id override (`anthropic`, `openai`, `gemini`, …) — pairs with a pinned `model`. | | `maxTokens` | int 1–64000 | Output cap. | | `max_tokens` | int 1–64000 | Same as `maxTokens` — the One API / snake_case spelling. | | `temperature` | number 0–2 | Sampling temperature. | | `top_p` | number 0–1 | Nucleus sampling. | | `reasoning_effort` | `low` \| `medium` \| `high` \| `xhigh` | Reasoning budget on models that expose one. | | `thinking_level` | string | Provider thinking-level id (Gemini-style), forwarded to the task as-is. | | `streaming` | boolean | Default on — the engine streams the run internally so the answer lands the moment the model finishes; `false` switches that off. No token stream reaches an MCP client either way (results are one envelope). | | `session_id` | string | Start/continue a conversational agent session — reuse the same id across turns. | | `user_context` | object | Where/when the user is: `time_zone` (IANA), `locale`, `place`, `latitude`, `longitude`, `accuracy_m`, `source`. Merged over the gateway's network guess; a bad value is dropped (MCP 2.6.0). | | `format` | `markdown` \| `json` | Response serialization; default markdown. | **Example** ```json {"user_request": "summarize this week's AI safety news", "agent": "one-agent", "prompt": "Summarize the most important AI safety developments this week, with sources."} ``` **Returns:** an outcome envelope. `ok` → the agent's final markdown + execution-id footer. A conversational agent that stops mid-dialog returns `needs_approval` / `needs_input` with a ⏸ block → relay and use `plungeai_continue`. **Failures & fixes** - Unknown agent id → `unavailable`: "No such agent ... ids cannot be guessed" + live alternatives. Re-discover; never retry the same id. - Inactive agent id → `unavailable`: "not `status:active`". Pick an alternative from the remediation or a fresh search. - Unknown `model` → `needs_input` BEFORE anything runs, with near-match catalog models. Omit `model` or pick a suggested catalog id. (The preflight exists because a bogus model would otherwise time out minutes later with no cause.) - Structured tool-agent called with prose → may run its default operation or reject inputs; switch to the contract + `plungeai_execute_tool`. --- ## plungeai_get_result **Purpose:** retrieve the output of one of the caller's executions by execution id — the id from any run's footer, from `plungeai_executions`, or from `plungeai_schedule {action: "runs"}`. Read-only, ownership-checked. **Parameters** | Param | Type | Notes | |---|---|---| | `execution_id` | string | **Preferred** id param (matches `run_mission`/`get_workflow_status`/`continue` output). | | `workflow_id` | string | Back-compat alias for `execution_id`. Engine ids (`exec-...`, hyphen) also resolve. | | `task_id` | string | Read one step's output instead of the final result. Step ids come from the result's own "Steps in this run" index. | | `format` | `markdown` \| `json` | Response serialization; default markdown. `json` returns the run's status record (as `GET /v1/executions/{id}`) plus `result` and `tasks` — the structured result, not the rendered thread (MCP 2.6.0). | Exactly one of `execution_id`/`workflow_id` is required (enforced at call time, not by the schema — both stay optional in the advertised tool signature). **Example** ```json {"user_request": "show me the full result of that run", "execution_id": "d3adb33f-...."} ``` **Returns (full result):** an identity header (workflow name — the question — timestamp — execution id), the final output verbatim, then as applicable: a **Steps in this run** index (multi-task runs persist only the final task's output; every other step is readable via `task_id`), the full **Conversation thread** (all follow-up turns, Studio-parity), and a ⏸ continuation block if the run is paused. Show it to the user whole. **Failures & fixes** - "No result found for <id>, or it is not one of your executions." → wrong or foreign id; take the id from the run's own footer or `plungeai_executions {action: "list"}`. - "still running. Poll plungeai_get_workflow_status, then retry." → do that; don't hammer get_result. - "This run failed: <reason>" → surface the reason; fix per its content (often a `needs_*` classification on the original call). - "completed, but its stored result is no longer retrievable (expired from SharedMemory)" → step outputs and old results expire; only the final result blob is durable. Re-run if the user needs it again. - Scheduler run ids (`exec_...`, underscore) never resolve here → use `plungeai_schedule {action: "runs"}` and take its **Execution ID** column (see `plungeai-scheduling`). --- ## Putting it together — the canonical single-agent run ``` 1. plungeai_list_agents {search: "summarize a pdf"} → one-agent (or a domain agent) 2. plungeai_execute_agent {agent: "one-agent", prompt: "Summarize the attached brief in three bullets."} → ok, final markdown + execution id 3. plungeai_get_result {execution_id: "<id>"} → re-fetch the same output later, or a step's output by task_id ``` For long-running prompt-driven work, wrap it in a one-task workflow via `plungeai_execute_workflow {mode: "async"}` (`plungeai-workflows`) — a bare `plungeai_execute_agent` call has no async mode. # Agents — the registry, discovery, and execution Source: https://www.plungeai.com/docs/skills/plungeai-agents/references/registry <!-- sources-of-truth: docs/architecture.md, orchestration/CLAUDE.md (registry), orchestration/mcp-gateway/server.ts, orchestration/api-gateway/openapi.ts, docs/PLUNGE-AI-AGENTS.md | last-synced: 2026-09-24 (re-verified: ~10-20ms RPC overhead claim against docs/architecture.md, GET /v1/agents and /v1/agents/categories shapes against routes/agents.ts — all match, no drift found) --> An **agent** is a deployed capability with a uniform interface: it accepts a task, does one job well (search the web, call an LLM, convert a document, post to Slack, enrich a CRM contact), and stores its result where the platform can hand it to the next step. Agents are the building blocks everything else composes: workflows chain them, missions call them, tools are a structured sub-species of them. The catalog is **live and active-only**: every listed agent is deployed and callable, and execution refuses any id that is not currently active. That is why ids must come from a live lookup, never from memory or an example you saw yesterday. ## Two kinds of agent — pick the right execution door | Kind | How you recognize it | How to execute | |---|---|---| | **Prompt-driven** | Card describes free-text input (e.g. `one-agent` — the LLM agent, with an optional `persona` — and search agents like `brave-agent`, `exa-agent`) | `plungeai_execute_agent` (MCP) or `POST /v1/agents/{id}/execute` (One API) with a `prompt` | | **Structured tool-agent** | Card carries a **Parameters table** / operations (e.g. document converters, weather, data-table, calendar agents) | Fetch the contract first (`plungeai_get_tool_contract` / `GET /v1/tools/{id}`), then `plungeai_execute_tool` / `POST /v1/tools/{id}/execute` with typed `params` — see the `plungeai-tools-connectors` skill | Sending prose to a structured agent, or typed fields to a prompt-driven one, is the most common execution mistake. The agent card tells you which kind you have. ## Terminology trap: "my agents" Users call their **saved workflows** "agents" too. An unqualified "show me my agents" means their saved workflows → `plungeai_list_workflows`. The registry (`plungeai_list_agents`) is the platform capability catalog — building blocks like `exa-agent` — and is what YOU use when selecting components. Only use the registry listing when the user explicitly asks about platform capabilities, or when you are composing. ## Discovery ### MCP: `plungeai_list_agents` `search` is a **hybrid semantic + keyword** query over live agent cards. Describe the capability in natural language and trust the ranking: ``` plungeai_list_agents {search: "convert pdf to markdown"} plungeai_list_agents {search: "web search"} plungeai_list_agents {category: "financial"} plungeai_list_agents {agent_id: "exa-agent"} # full card for one agent ``` - Prefer `search`/`category` filters. An unfiltered listing returns the whole catalog (large, slow to read, rarely what you want — discover, don't page through). - Fetch the **full card** (`agent_id`) before first use of an unfamiliar agent. Cards carry: operations, parameters, good-at examples, and — critically — **"Not for → use X instead"** redirects that save you a wrong pick. ### One API ```bash # List active agents (paged) curl -s "https://api.plungeai.com/v1/agents?limit=20&offset=0" \ -H "Authorization: Bearer ozk_YOUR_KEY" # Hybrid search over the whole registry (agents, mcp-servers, …) curl -s "https://api.plungeai.com/v1/discovery/search?q=web%20search&kind=agents&fields=summary" \ -H "Authorization: Bearer ozk_YOUR_KEY" # Ask the platform to recommend the best card for a task curl -s -X POST https://api.plungeai.com/v1/discovery/recommend \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"type": "agent", "task": "find recent news about a company"}' # Full card, markdown LLM view curl -s https://api.plungeai.com/v1/discovery/cards/agent/exa-agent \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` Search response envelope: `{cards: [...], count, searchMethod}`. Each card carries `id, name, type, category, status, description, tags`. Add `include=quality` to get a `quality` block per card (`score`, `success_rate`, `runs` — `null` when unmeasured); use it to prefer proven agents when several match. `mode=hybrid|keyword|vector` forces a search leg; the default hybrid is almost always right — do not re-implement ranking client-side. ## Categories (orientation, not an inventory) Agents are organized by domain: **search** (Brave, Tavily, Exa, Perplexity, Serper), **generic LLM** (`one-agent`; the legacy `llm-agent` / `skill-agent` ids stay callable until the switch-over), **documents**, **financial**, **CRM** (large family: enrichment, outreach, scoring), **coding**, **legal**, **healthcare**, **logistics**, **marketing**, **market research**, **social**, **payments**, **e-commerce**, **news**, **travel**, **design**, **automation**, **Google Workspace** (gmail, calendar, drive), **Microsoft 365**, and more. Category names are stable; membership is not — discover live. Stable id shape: kebab-case, usually `<thing>-agent` (`brave-agent`, `exa-agent`, `one-agent`). Beware near-misses: `brave-agent` exists, `brave-search` may not — take the id character-for-character from the search result. ## Execution ### MCP: `plungeai_execute_agent` ``` plungeai_execute_agent { agent: "one-agent", prompt: "Summarize the three biggest risks in this text: ...", session_id: "optional — start/continue a conversational session" } ``` - The id parameter is **`agent`** (not `agent_id`, which belongs to the tool-contract door). Optional per-call overrides: `persona`, `model`, `maxTokens`. - Returns a structured outcome. `ok` → the result content (final, user-ready markdown — relay verbatim). Anything else names the remediation (see SKILL.md "Trust fences"). - `session_id` makes conversational agents stateful across calls. - **This tool has no `mode` parameter — it is always sync.** For a long single-agent run, use the One API with `"sync": false` (below), or run it as a one-task workflow via `plungeai_execute_workflow {mode: "async"}` → poll `plungeai_get_workflow_status`, fetch with `plungeai_get_result`. ### One API: `POST /v1/agents/{id}/execute` ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{ "prompt": "One-paragraph brief: the current state of solid-state batteries", "model": "claude-sonnet-5", "maxTokens": 2048, "sync": true }' ``` Sync response (default): ```json { "content": "…final markdown…", "workflow_id": "exec-…", "task_id": "…", "request_id": "…" } ``` `input` is an alias for `prompt`; `persona` injects a persona (see the `plungeai-skills-plugins` skill); `model`/`maxTokens` override per call. **Async:** `"sync": false` → `202` with `{workflow_id, task_id, request_id}`. Redeem: ```bash curl -s https://api.plungeai.com/v1/agents/results/{workflowId}/{taskId} \ -H "Authorization: Bearer ozk_YOUR_KEY" # 200 {content, content_type, workflow_id, task_id} | 404 {error: {code: "not_ready"}} ``` `404 not_ready` means still running — poll again; it is not a failure. ### Errors you will actually see | Signal | Meaning | Do | |---|---|---| | `400 missing_prompt` | No prompt/input in body | Send one | | refusal naming the agent as parked/unknown | Id not active in the live catalog | Re-search the registry; take a live id | | `502 engine_error` / `result_unavailable` | Downstream execution failed | Read the message; check traces (`plungeai-results-traces`); do not hammer-retry | | Outcome `needs_connection` | Agent needs a user credential (e.g. Google OAuth) | Tell the user what to connect in Studio, then retry | ## Under the hood (why it behaves this way) Every agent implements the same interface — `executeTask(yaml) → string` over Cloudflare service-binding RPC — which is what makes agents interchangeable inside workflows and lets the engine fan them out in parallel with ~10-20 ms dispatch overhead. Results are written to SharedMemory keyed by `(workflow_id, task_id, your user id)`, which is why every execution — even a single agent call — hands back a `workflow_id`/`task_id` pair you can redeem later. See the `plungeai-memory` skill for retrieval and `plungeai-workflows` for composition. ## Checklist before any agent call 1. Id from a live search (never memory). 2. Full card read if unfamiliar — right kind (prompt vs structured), right agent ("Not for" redirects honored). 3. Right door: prompt-driven → execute_agent; structured → contract + execute_tool. 4. Long run → async mode. 5. Outcome remediation followed; fences surfaced, never retried blind. # Agents plane — /v1/agents Source: https://www.plungeai.com/docs/skills/plungeai-agents/references/rest-agents-plane <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/agents.ts, core/core-base/credential-tiers.ts, orchestration/api-gateway/docs/guide-3.0/09-agents.md | last-synced: 2026-10-05 (One API 2.3.0: credential on list cards, 424 connect pointer, approve round-trip, results 409/422, persona-only forwarding; earlier re-verified error catalogue, 403 agent_not_active / 404 unknown_agent pre-dispatch fence, and outcomeStatus mapping against routes/agents.ts, lib/agent-fence.ts and routes/_util.ts — all match, no drift found) --> Execute any active platform agent with a plain prompt. The gateway wraps your call in a one-task workflow, so every execution yields a `workflow_id` + `task_id` pair — that pair is the handle for async redemption and for traces. Auth: `Authorization: Bearer ozk_YOUR_KEY` on every route here. ## GET /v1/agents — list active agents Query params: `limit` (default 50, max 100), `offset` (default 0). ```bash curl -s "https://api.plungeai.com/v1/agents?limit=2" \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "agents": [ { "id": "one-agent", "name": "One Agent", "type": "agent", "category": "ai", "status": "active", "description": "General-purpose LLM agent: one-shot calls or a bounded agentic loop", "tags": ["llm", "completion"], "credential": { "tier": "platform" } }, { "id": "brave-agent", "name": "Brave Search", "type": "agent", "category": "search", "status": "active", "description": "Web search via Brave", "tags": ["search", "web"], "credential": { "tier": "platform" } } ], "count": 2 } ``` Each entry is a registry `Card` (summary fields) plus its `credential` tier: `{"tier": "platform"}` or `{"tier": "connection", "provider", "method"}` (the agent acts as the caller; connect `provider` first — see `plungeai-tools-connectors`). `count` is the returned page's length, not the catalog total — page until a short page, never until `offset >= count`. A failed registry hop returns `502 upstream_error`. The catalog is live — take ids verbatim from this listing or from `GET /v1/discovery/search`; never from memory. For richer capability search (semantic, filtered, quality scores) use the discovery plane — see the `plungeai-discovery` skill. ## GET /v1/agents/categories — agent categories with counts Agent categories with active-agent counts, aggregated from the registry (the registry has no categories endpoint of its own). **Not** a cache-stats pass-through — an older version of this route proxied `/api/registry/stats` and returned cache statistics instead; that shape is gone. ```bash curl -s https://api.plungeai.com/v1/agents/categories \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "categories": [ { "name": "search", "count": 12 }, { "name": "documents", "count": 9 } ], "count": 2 } ``` `categories[]` is sorted by count descending, then name. `count` is the number of distinct categories, not the total agent count. The aggregation queries only the first 100 active agents from the registry (one page, `limit=100&offset=0`) — on a catalog past 100 active agents this is a representative sample of category names, not a guaranteed-complete count over every active agent. A failed registry hop returns `502 upstream_error`, same as `GET /v1/agents`. ## POST /v1/agents/{id}/execute — run an agent Body fields: | Field | Type | Notes | |---|---|---| | `prompt` | string | The instruction. **Required** (or `input`) | | `input` | string | Alias for `prompt` | | `persona` | string | Optional persona overlay — the only capability field this route forwards (`skills`, `experts`, `backgrounds`, `plugins`, `mcp` are ignored; use a `type: harness` task) | | `provider` | string | Provider id (`anthropic`, `openai`, `gemini`, …); required alongside a pinned `model` | | `model` | string | Optional model override for LLM-driven agents | | `maxTokens` | integer | Optional completion cap (`max_tokens` accepted as an alias — OpenAI habits work) | | `max_tokens` | integer | Alias for `maxTokens` | | `temperature` | number | Sampling temperature, forwarded to the provider | | `top_p` | number | Nucleus sampling probability, forwarded to the provider | | `reasoning_effort` | string | Reasoning-effort hint (e.g. `low`/`medium`/`high`) for reasoning models | | `thinking_level` | string | Extended-thinking level hint for models that support it | | `system` | string | System prompt / instructions for the run | | `messages` | array | Prior chat messages (`[{role, content}]`) instead of a single `prompt` | | `user_context` | object | Caller time zone, locale and place (`{time_zone, locale, place, latitude, longitude, accuracy_m, source}`); derived from the request's network location when omitted | | `approved`, `action_token`, `allow_scope` | boolean, string, string | The approve round-trip for a paused turn: send the token the paused reply carried with `approved: true` (both, or `400 invalid_params`); `allow_scope` is `once` (default), `run` or `always` | | `sync` | boolean | Default `true`. `false` → 202 pointer | | `stream` | boolean | Default `false`. `true` → an OpenAI-shaped `chat.completion.chunk` SSE instead of a JSON result (see "Streaming" below). Ignored on the async pointer path | | `format` | string | Response negotiation: `json` (default) \| `yaml` \| `markdown` \| `text` — same values as an `Accept` header (`application/json`, `text/yaml`, `text/markdown`, `text/plain`) or the request `Content-Type` mirror | ### Sync (default) — result inline ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "Summarize in one line: Cloudflare Workers are serverless."}' ``` ```json { "content": "Cloudflare Workers run code at the edge without managing servers.", "workflow_id": "00000000-0000-4000-8000-000000000001", "task_id": "t1", "request_id": "00000000-0000-4000-8000-000000000004" } ``` ### Async — 202 + pointer Use for long-running prompts, or when your caller can't hold a connection. ```bash curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "Write a 2000-word market analysis of edge computing", "sync": false}' ``` HTTP **202**: ```json { "workflow_id": "00000000-0000-4000-8000-000000000001", "task_id": "t1", "request_id": "00000000-0000-4000-8000-000000000004" } ``` ### Streaming (`stream: true`) Instead of a JSON result, the 200 response is `text/event-stream`: an OpenAI-shaped `chat.completion.chunk` per line — ``` data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1788574344,"model":"one-agent","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]} data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1788574344,"model":"one-agent","choices":[{"index":0,"delta":{"content":"391"},"finish_reason":null}]} data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1788574344,"model":"one-agent","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} data: [DONE] ``` One primer chunk (`delta.role: "assistant"`), then a `delta.content` chunk per provider token, then a final chunk (`delta: {}`, `finish_reason: "stop"` or `"length"` when truncated at `max_tokens`), then a literal `data: [DONE]` line. `usage` is emitted on the final chunk only when the provider reports the prompt/completion split (often omitted for streamed runs). Comment keepalives (`: OCEAN PROCESSING`) may appear every ~15s while idle. Response header `X-Execution-Id` carries the execution id for result redemption / polling regardless of `stream`. An error before the first byte is a normal JSON error envelope (not SSE); an error after the first byte is a single `data: {"error":{…}}` frame and the stream closes WITHOUT `[DONE]`. ### Errors | Status | Code | Meaning | |---|---|---| | 400 | `missing_prompt` / `invalid_format` | No prompt/input given, or `format` isn't one of `json`/`yaml`/`markdown`/`text` | | 401 | `unauthorized` | Bad/missing `ozk_` key | | 403 | `agent_not_active` | The agent card is not `status:active` (refused pre-dispatch) | | 404 | `unknown_agent` | No such agent id in the registry (refused pre-dispatch) | | 409 | `duplicate_execution_id` | The `x-trace-id` header was already used by an earlier run — send a fresh UUID | | 413 | `payload_too_large` | Body over 1 MiB (`MAX_REQUEST_SIZE`) | | 422 | `invalid_params` / `unknown_model` / `empty_completion` | Missing/invalid input named by the agent; pinned model absent/inactive in the catalog; or the run completed but the model returned no visible content — raise `max_tokens` ≥ 64 or change the model | | 424 | `connection_required` / `credential_required` | The agent needs a connection or key the user hasn't set up; `error.connect` is `{provider, method, link}` (`link` is the route `POST /v1/connections/<provider>/link`, not a URL) | | 429 | `rate_limited` | `Retry-After` header | | 502 | `engine_error` / `result_unavailable` | Engine dispatch failed, or sync redemption couldn't fetch the stored result | | 502 | `upstream_error` | A downstream hop threw (catch-all, any route in this plane) | | 503 | `agent_unavailable` | Agent temporarily down | A `502 result_unavailable` on a sync call does NOT always mean the run failed — the result may land late, and its error body carries `workflow_id` + `task_id`. Redeem with those ids at the results route below before re-running. ## GET /v1/agents/results/{workflowId}/{taskId} — redeem an async result ```bash curl -s https://api.plungeai.com/v1/agents/results/00000000-0000-4000-8000-000000000005/t1 \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` Ready — HTTP 200 (`StoredResult`): ```json { "content": "# Edge Computing Market Analysis\n\n...", "content_type": "text/markdown", "workflow_id": "00000000-0000-4000-8000-000000000001", "task_id": "t1" } ``` Not ready yet — HTTP 404: ```json { "error": { "code": "not_ready", "message": "result not available (yet)" } } ``` Results are tenant-scoped: you can only redeem pointers created by your own key's identity — a pointer that is not yours (or never existed) answers the same `404 not_ready` as a run still in flight, so always cap the poll loop. The terminal answers: `409 execution_failed` (the message is the run's error) and `422 empty_completion` (the run completed but the model returned no visible content; `hint: max_tokens >= 64`). ### Polling pattern `404 not_ready` is the normal in-flight state. Poll with backoff, cap the wait: ```bash WF=00000000-0000-4000-8000-000000000005; TASK=t1 for i in 1 2 3 4 5 6 7 8; do sleep $((i * 2)) BODY=$(curl -s -w '\n%{http_code}' \ "https://api.plungeai.com/v1/agents/results/$WF/$TASK" \ -H "Authorization: Bearer ozk_YOUR_KEY") [ "$(echo "$BODY" | tail -1)" = "200" ] && { echo "$BODY" | sed '$d'; break; } done ``` ```python import time, requests def redeem(wf, task, key, tries=8): for i in range(1, tries + 1): r = requests.get( f"https://api.plungeai.com/v1/agents/results/{wf}/{task}", headers={"Authorization": f"Bearer {key}"}) if r.status_code == 200: return r.json()["content"] if r.status_code != 404: r.raise_for_status() time.sleep(2 * i) # backoff; 404 not_ready is normal raise TimeoutError("result not ready") ``` ## Choosing sync vs async - **sync (default)**: single quick task, interactive callers. Simplest code. - **async (`sync:false`)**: anything that may run long; queue workers; retries survive process restarts because the pointer is durable. - Send an `x-trace-id` header on the execute call and you can watch the run's spans at `GET /v1/traces/<your-trace-id>` while polling — see the `plungeai-results-traces` skill. # plungeai-campaigns Source: https://www.plungeai.com/docs/skills/plungeai-campaigns Run a list to completion on PlungeAI: the campaign ledger (claim/complete/fail/retry), the campaign-config (list source, cycle, cadence, retries, delivery), and how a campaign keeps going via cron or self-scheduling runAgain. A campaign is a saved workflow of kind `campaign`: create it over the One API (`POST /v1/workflows` with `kind: campaign` + `campaign_config`), MCP (`plungeai_workflow`) or Studio. Use when the user wants to process every row of a table on a cadence, run a batch job until a list is drained, or build a continuous monitor over a fixed set of items. Ledger status, pause and stop are Studio-session routes (`ocean-cli campaigns`); authoring UI: `plungeai-campaign-agent`; for the pipeline steps use `plungeai-workflows`. [Download zip](https://skills.plungeai.com/plungeai-campaigns.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-campaigns/SKILL.md) A campaign claims a list of items and works through them with per-item retries on a repeating cadence, until the cycle is drained — the pattern behind "check every row in this table weekly" or "watch these 50 URLs continuously." Its runtime is the **campaign ledger**: a small set of `agent: data-table-agent` operations (claim/complete/fail/release/status) that a CNL `type: batch` task with `ledger: campaign` calls automatically. ## Prerequisites - A self-service `ozk_` API key from **Dashboard → One API → Keys** (https://dashboard.plungeai.com) for REST (`https://api.plungeai.com`) and MCP (`https://mcp.plungeai.com/v1`), or build the pipeline in Ocean Studio. - **A campaign is a saved workflow with `kind: campaign`.** REST, MCP and Studio create the same row; the ledger operations below are the engine's own (`data-table-agent` is an internal agent with no registry card — you do not discover or call it, you write it into the campaign YAML and the platform arms the ledger at save). ## Over the One API, MCP and CLI | Job | REST (`ozk_` key) | MCP | CLI `ocean-cli …` | |---|---|---|---| | create | `POST /v1/workflows {name, kind: "campaign", yaml, campaign_config, folder?, description?}` → `201 {workflow, warnings?}` | `plungeai_workflow {action: "create", kind: "campaign", name, yaml, campaign_config}` | `workflows create` then `campaigns setup ID --config @campaign.json` (Studio session) | | list, read | `GET /v1/workflows?kind=campaign` → `{workflows, total, limit, offset, has_more}`; `GET /v1/workflows/{id}` | `plungeai_list_workflows {kind: "campaign"}`; `plungeai_workflow {action: "get"}` | `workflows list --kind campaign` | | change, delete | `PATCH /v1/workflows/{id}`; `DELETE /v1/workflows/{id}` → `204`; versions: `GET\|POST /v1/workflows/{id}/versions` | `plungeai_workflow` `update` / `delete` / `save_version` | `workflows update`, `delete`, `versions` | | cadence | the campaign's job is a schedule: `GET /v1/schedules`, then `POST /v1/schedules/{id}/pause` \| `resume` \| `run` | `plungeai_schedule` `list` / `pause` / `resume` / `run_now` | `schedules …` | | a campaign's runs | `GET /v1/executions?workflow_id=<campaign id>` | `plungeai_executions {action: "list"}` | `executions list` | | ledger status, pause, stop | — (Studio session only) | — | `campaigns status \| pause \| resume \| stop \| complete ID` | ```bash curl -s -X POST https://api.plungeai.com/v1/workflows \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d "$(jq -n --rawfile yaml watch.yaml '{name: "Status-page watch", kind: "campaign", yaml: $yaml, campaign_config: {list: {inline: ["https://status.example-a.com", "https://status.example-b.com"]}, cycle: "continuous", schedule: "0 * * * *", max_attempts: 3}}')" ``` What builders get wrong: - **`campaign_config` is JSON on REST and MCP** (the `campaign-config` fenced block is the Studio spelling of the same fields): `list` with exactly one of `table`, `agent`, `csv`, `inline`; `cycle`; `max_attempts` (integer ≥ 1) are required; `cycle_start`, `schedule` (5-field cron), `batch_size`, `max_items`, `pace`, `then`, `deliver`, `local` are optional. `422 invalid_params` names the field and nothing is created; `kind` other than `campaign` with a `campaign_config` is `422` too. The Studio-only `task_types` and `learn` keys are dropped. - **A `201` can still carry `warnings`**: `campaign setup did not complete — the workflow is saved without its ledger`, `Ledger not created: …`, or a rejected `schedule`. Read them; the row exists either way. - **No `schedule` means no scheduler job.** The ids of the ledger and the job are not in the `201`: find the job with `GET /v1/schedules` (named `<name> - Campaign`). For a campaign you build over the API, set `schedule`. - **The save is linted** (`422 invalid_workflow`, `errors` listed): exactly ONE `agent: data-table-agent`, `operation: campaignClaim` task and ONE `type: batch` task with `items_from` and `ledger: campaign`; `concurrency` ≤ 5; no `ask_user` or `platform_action` inside the batch (it is unattended); an inner `type: harness` task lists `task_complete` in `allowed_tools`; `max_items` is required when an inner task can reach `data-table-agent`. - **`DELETE` is a teardown, not a cascade**: the campaign's jobs go and its ledger header is tombstoned, results stay. The same teardown runs when a `PATCH` moves `kind` away from `campaign`. - Every route is yours only: another account's campaign id is `workflow_not_found`, like a missing one. ## Discover the pieces The items a campaign processes still come from ordinary discovery — never guess an agent id for the per-item work: `plungeai_list_agents {search: "<what each item needs done>"}` (REST: `GET /v1/discovery/search?kind=agents&q=…`). The ledger agent `data-table-agent` is the exception: it is internal to the engine, has no discoverable card, and is named only in the claim task of the campaign YAML. ## The shape 1. **`campaignClaim`** — a `type: task` leases up to `batch_size` pending items for this run. 2. **`type: batch` with `ledger: campaign`** — fans out over the claimed items (`items_from: claim`, `concurrency: N`); each per-item task's success/failure is written back to the ledger automatically as `campaignComplete`/`campaignFail`. 3. **A campaign config** tells the platform the list source, cycle (`once`/`hourly`/`daily`/`weekly`/`monthly`/`continuous`), cron `schedule`, `max_attempts`, and optional delivery/chaining: the `campaign_config` JSON on REST and MCP, a `campaign-config` fenced block after the YAML in Studio. ```yaml workflow: name: "Status-page watch" tasks: - type: task id: claim agent: data-table-agent operation: campaignClaim campaign_id: "<written at save>" batch_size: 50 - type: batch id: work items_from: claim concurrency: 5 ledger: campaign tasks: - type: task id: one agent: firecrawl-agent operation: scrape url: "{item.key}" formats: [markdown] ``` ```campaign-config list: { inline: ["https://status.example-a.com", "https://status.example-b.com"] } cycle: continuous cycle_start: reset schedule: "0 * * * *" max_attempts: 3 deliver: [{ channel: slack }] ``` ## Keeping it going A campaign with a `schedule` in its config runs on an ordinary recurring cron job. One WITHOUT a schedule ends its pipeline with `agent: scheduler, operation: runAgain, in: "1h"` — a self-scheduling `@once` job that never stacks. Both mechanisms and full cron/date-token detail: `plungeai-scheduling`. ## Gotchas - **`upsertRow` is insert-only, not keyed** — it does not match `matchFields`; use `chainStep` or explicit `col_<name>` columns for an idempotent per-item write, or `updateRows` to change an existing row. - Never chain (`then`) a dependent *analysis* report onto a data-collection campaign — a partial cycle must not trigger it; keep such reports time-triggered. - The scheduler's zero-progress breaker auto-pauses a campaign after several consecutive runs that complete nothing — check `campaignStatus` before assuming it needs a manual nudge. - Every ledger operation is scoped to the caller's own account — there is no cross-tenant campaign surface. ## Related skills - `plungeai-campaign-agent` — the Studio kind that authors campaigns visually (kept id — this skill documents its underlying mechanics, not a replacement UI). - `plungeai-workflows` — the `batch`/`task`/`harness` types a campaign pipeline is built from. - `plungeai-scheduling` — cron cadence, `@once`, and `runAgain` self-scheduling. - `plungeai-missions` — when a per-item step needs judgment instead of a fixed call. - `plungeai-results-traces` — reading a campaign run's execution history. ## Reference - [`references/campaigns.md`](/skills/plungeai-campaigns/references/campaigns) — the `campaign-config` field table, every ledger operation (`campaignBegin`/`Claim`/`Complete`/`Fail`/`Release`/`Status`/ `AddItems`/`RetryFailed`/`SetStatus`/`Delete`/task rows/`chainStep`/`counterStep`), sizing, delivery channels, and gotchas. ## Reference pages <CardGroup cols={2}> <Card title="Campaigns — running a list to completion (the campaign ledger)" icon="file-text" href="/skills/plungeai-campaigns/references/campaigns"> A campaign claims a list of items (a data-table query, an agent's lister output, an uploaded CSV, or an inline list), runs each item through a CNL pipeline… </Card> </CardGroup> # Campaigns — running a list to completion (the campaign ledger) Source: https://www.plungeai.com/docs/skills/plungeai-campaigns/references/campaigns <!-- sources-of-truth: agents/agents/campaign-agent/campaign-agent-design-5.0.md §8, agents/agents/campaign-agent/EXAMPLES.md, core/core-data-table-agent/README.md, core/core-data-table-agent/campaign-ledger.ts, core/core-data-table-agent/chain-step.ts, core/core-data-table-agent/counter-step.ts, orchestration/scheduler/README.md, orchestration/scheduler/CLAUDE.md, core/core-base/campaign-setup.ts, core/core-base/kind-lint.ts, orchestration/api-gateway/routes/saved.ts, apps/ocean-cli/src/commands/session/campaigns.ts | last-synced: 2026-10-05 (One API 2.3.0: kind campaign over REST/MCP, campaign_config JSON, save-time lint; earlier 2026-09-23) --> A **campaign** claims a list of items (a data-table query, an agent's lister output, an uploaded CSV, or an inline list), runs each item through a CNL pipeline with per-item retries, and repeats on a cadence until the list ("cycle") is drained — then either re-cycles (`refill`/`reset`) or hands off to another campaign (`then`). It is PlungeAI's answer to "process these 280 rows every Monday" or "watch these 50 URLs forever." **The surface today.** A campaign is a saved workflow of `kind: campaign`. Create it over the One API (`POST /v1/workflows` with `kind: "campaign"` and a `campaign_config` object), over MCP (`plungeai_workflow {action: "create", kind: "campaign", campaign_config}`) or visually in Ocean Studio via the **`plungeai-campaign-agent`** kind (CNL YAML + a `campaign-config` fenced block): all three write the same row and arm the same ledger (`campaignBegin`) and cron job. Listing, reading, changing and deleting are the ordinary workflow routes (`GET /v1/workflows?kind=campaign`, `PATCH`, `DELETE`). Ledger status, pause, resume, stop and completing a human row are Studio-session routes (`ocean-cli campaigns status | pause | resume | stop | complete | setup`); over an `ozk_` key you pause the campaign's *schedule* instead (`POST /v1/schedules/{id}/pause`). The ledger operations in this page are the engine's own: you write them into the campaign YAML, you do not call them directly. ## Discover the pieces first A campaign's pipeline is ordinary CNL, so discover its agents the normal way — never guess an id: `plungeai_list_agents {search: "<what the item-processing step needs>"}` (REST: `GET /v1/discovery/search?kind=agents&q=…`). `data-table-agent` is the exception: an internal engine agent with no registry card (a lookup answers "no such card"); it appears only in the claim task of the campaign YAML and in the ledger operations below. ## The `campaign-config` block A fenced ```` ```campaign-config ```` block sits after the CNL YAML; Studio parses it to open the ledger and create the scheduler job. Over REST and MCP the same fields are the `campaign_config` JSON object of `POST /v1/workflows` / `plungeai_workflow` (a `list` object instead of the block's `list:` line; `task_types` and `learn` are Studio-only and dropped there). | Field | Required | Meaning | |---|---|---| | `list` | yes | Where items come from: `table:` (a data-table project+table), `agent:` (a lister task whose JSON array feeds `items_from`), `csv: true` (uploaded rows), or `inline: [key, …]`. | | `cycle` | yes | Calendar bucket: `once` \| `hourly` \| `daily` \| `weekly` \| `monthly` \| `continuous`. | | `cycle_start` | no | `refill` (add new list rows, keep prior) or `reset` (fresh cycle from the list). Default over REST/MCP: `reset` for `csv` / `inline`, else `refill`. | | `schedule` | no | 5-field cron for the run cadence, e.g. `"*/5 9 * * 1"` — the scheduler re-fires until the cycle drains (`plungeai-scheduling`). Without it no scheduler job is created and the pipeline schedules its own next run (`runAgain`, below). | | `max_attempts` | yes | Per-item retry cap before an item is marked `failed` (integer ≥ 1; the ledger column defaults to 2). | | `then` | no | Campaign or workflow id (`A-Za-z0-9_-`, ≤ 64) to hand the baton to when this one's cycle completes. | | `deliver` | no | `[{channel, to?}]` for the cycle-complete notice (below); `channel` is `inapp` (default), `email`, `slack`, `telegram`, `whatsapp`, `discord` or `teams`. | | `local` | no | `true` runs items on the user's own computer. | | `max_items` | no | Hard cap on total items claimed across the cycle (required when an inner task can reach `data-table-agent`). | | `batch_size`, `pace` | no | Positive integer claim slice per run (default 40); seconds between runs (`pace` is inert today). | | `task_types` | no | Per-row task/worker overrides. | | `learn` | no | Enables the ledger's recall/knowledge lane. | ## The CNL shape — claim, batch, complete ```yaml workflow: name: "RH price check" tasks: - type: task id: claim agent: data-table-agent operation: campaignClaim campaign_id: "<written by Studio at save>" batch_size: 40 - type: batch id: work items_from: claim concurrency: 5 ledger: campaign tasks: - type: task id: one agent: price-collector-v2-agent operation: collect_single product_name: "{item.key}" week_date: "{item.cycle}" ``` `type: batch` with `ledger: campaign` is what wires the batch's per-item outcome back into the campaign ledger automatically (complete/fail bookkeeping) — see `plungeai-workflows` for the general `batch` task type. An item's work can be a plain `task` (as above) or an open-ended `type: harness` mission (`plungeai-missions`) when the per-item work needs judgment, not a fixed call. ## The ledger primitives (`agent: data-table-agent` operations) Every operation is owner-scoped (`owner_uuid` — resolved from your account, not a field you pass by hand): | Operation | Purpose | |---|---| | `campaignBegin` | Create/upsert the campaign header at save time (list source, cycle kind, batch/concurrency/max_attempts, optional seed items). | | `campaignClaim` | Lease up to `batch_size` pending items for this run (`run_id`); opens a new cycle if the prior one drained and `cycle_start` allows it. Refuses to lease from a non-`active` campaign. | | `campaignComplete` | Mark one leased item done with its result — only a currently-leased row transitions (a stale duplicate is a safe no-op). | | `campaignCompleteHuman` | Complete a `worker_kind: "human"` row directly — human rows are never leased, so this is the only way one closes out. | | `campaignFail` | Record a failed attempt; the item returns to `pending` until `max_attempts`, then flips to `failed`. | | `campaignRelease` | Un-lease claimed-but-unfinished rows (e.g. a crashed run) back to `pending`. | | `campaignStatus` | Read the header + per-status item counts. | | `campaignAddItems` | Add items to the current (or a named) cycle; refuses past `max_items`. | | `campaignRetryFailed` | Re-queue `failed` items (optionally a specific key list) back to `pending`. | | `campaignSetStatus` | Pause/resume/mark done; pausing stamps `paused_by` (default `user:<owner>`; the scheduler's own zero-progress breaker stamps `auto:zero-progress`). | | `campaignDelete` | Soft-delete (`deleted_at` + `status='done'`); releases held leases; history stays queryable. | | `campaignTasks` / `campaignUpsertTasks` / `campaignSetTaskActive` | Read/edit the campaign's editable task-row list (the Task-app grid). | | `campaignResults` / `campaignPivot` | Recent per-item results (optionally rolled up per cycle) / a tasks-×-runs reporting grid. | | `chainStep` | The zero-AI "forever campaign" unit of work: bump a lane counter once per cycle (idempotent under at-least-once re-claims) and log a per-run row. | | `counterStep` | A pure per-cycle counter (`prev + step`), for the simplest possible repeating tally. | Full parameter shapes live in `core/core-data-table-agent/campaign-ledger.ts`, `chain-step.ts`, `counter-step.ts` — read those before hand-building a pipeline around one of these operations; nothing here is invented beyond what that code implements. ## Cadence — two ways a campaign keeps going 1. **Cron `schedule`** — an ordinary recurring scheduler job re-fires the pipeline until the cycle drains (the Forever Campaign ran on `*/5 * * * *`). This is the normal path when `campaign-config.schedule` is set. 2. **`runAgain` self-scheduling** — a campaign saved *without* a cron schedule ends its pipeline with `agent: scheduler, operation: runAgain, in: "1h"` (or another duration). This creates exactly ONE `@once` job for the workflow and **never stacks** — an existing pending `@once` job for the same workflow is re-timed, not duplicated. See `plungeai-scheduling` for `@once`/`runAgain` mechanics and cron/date-token reference in full. ## Sizing Keep `batch_size × seconds-per-item ÷ concurrency ≤ 600s` — comfortably inside the scheduler's execution window. The defaults (`batch_size: 40`, `concurrency: 5` at ~30s/item ≈ 4 minutes per run) are safe up to roughly 100 items per scheduled run; `batch_size`/`concurrency` are hidden authoring knobs, not something you expose to an end operator. ## Delivery on cycle-complete (`deliver`) - Fire-and-forget: a delivery failure never fails the run. - Length caps per channel: telegram 3800, whatsapp 3800, discord 1900, slack 3800, email 100000, inapp 2000 characters — keep output well under the smallest cap you use. - telegram / whatsapp targets must be paired to the owner first (a pairing code from Studio, `/pair <code>` from that chat); discord requires any active pairing. Unpaired targets are skipped. - email sends from the platform address; a bare target means the owner's address — any other address needs that holder's prior confirmation, else it's skipped silently. - slack: a bare target DMs the owner; a specific `chat_id`/`to` needs the owner to be a connected workspace member. ## Saving one over the API — validation `POST /v1/workflows` lints a `kind: campaign` YAML before anything is written (`422 invalid_workflow`, `errors` listed): exactly one claim task (`agent: data-table-agent`, `operation: campaignClaim`) and exactly one `type: batch` task with `items_from` (the claim task id) and `ledger: campaign`; `concurrency` ≤ 5 (open-connection cap); no `ask_user` / `platform_action` inside the batch (unattended); an inner `type: harness` task must list `task_complete` in `allowed_tools`; `max_items` is required when an inner task can reach `data-table-agent`. A missing `{item.key}` in the per-item task is a warning (the ledger is at-least-once: write idempotently). Setup then stamps the campaign id into the claim and batch tasks, opens the ledger and, when `schedule` is set, creates the cron job named `<name> - Campaign`. Failures after the row is saved come back as `warnings` on the `201`. ## Gotchas - **`upsertRow` is insert-only, not keyed** — it does not match `matchFields` or update in place, and a JSON-string `data` is not parsed. For an idempotent per-item write use `chainStep` or explicit top-level `col_<name>` columns (`insertRow`/`upsertRow` substitute `{item.*}` only into top-level string fields); to update an existing row use `updateRows`. - A partial cycle should never trigger a *dependent* report via `then` — chain only full completions to full completions (a data-collection campaign feeding a time-triggered analysis job is the safer pattern than chaining the analysis itself). - The zero-progress breaker auto-pauses a campaign after several consecutive completed-runs-that-completed-nothing (an all-failing list) — check `campaignStatus`/the Task app before assuming a stalled campaign needs a manual nudge. - Every ledger operation is scoped by the caller's own `owner_uuid` — there is no cross-tenant campaign management surface. # plungeai-discovery Source: https://www.plungeai.com/docs/skills/plungeai-discovery Find the right thing on PlungeAI's live registry before building anything: agents, structured tools, models, skills, personas, connectors, and workflow templates, via hybrid semantic + keyword search (`plungeai_list_agents` MCP / `GET /v1/discovery/search` REST / `ocean-cli discovery search`), full capability cards, a recommendation (`POST /v1/discovery/recommend`) and a one-answer resolver (`POST /v1/discovery/resolve`), tool-invocation contracts (`plungeai_get_tool_contract` / `GET /v1/tools/{id}`), and the template gallery (`plungeai_templates` / `GET /v1/templates`). Use when picking an agent/tool/model/skill for a job, before the first call to an unfamiliar agent, deciding what to build from, or debugging a refused/unknown id. Ids are live and active-only — never assert one from memory. For running a prompt-driven agent use `plungeai-agents`; for typed tool execution `plungeai-tools-connectors`; for the model catalog `plungeai-models`; for CNL authoring `plungeai-workflows`. [Download zip](https://skills.plungeai.com/plungeai-discovery.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-discovery/SKILL.md) The platform's capability catalog — agents, tools, models, skills, personas, connectors, workflows — is **live and changes without notice**. Discovery is not an optional first step, it is the mechanism that makes every other capability usable: an id you did not just get from a live lookup is likely to be refused at execution. ## Prerequisites - Self-service `ozk_` key from **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`). - MCP: `https://mcp.plungeai.com/v1`. REST: `https://api.plungeai.com`. ## Discovery first (this IS the discovery-first step) Never name an agent, tool, connector, or model in generated code, a workflow, or a reply unless that exact id came from one of the calls below in this session. "90-something agents" is flavor; the live catalog is the fact. ## 1. Search by capability — MCP ```json plungeai_list_agents {search: "convert pdf to markdown"} plungeai_list_agents {kind: "skills", search: "…"} plungeai_list_agents {agent_id: "exa-agent"} // full card ``` `kind` filters the catalog segment: `agents` (default, active-only) \| `personas` \| `experts` \| `skills` \| `models` \| `workflows` \| `connectors`. Full parameter table and failure modes — [`references/list-and-contract.md`](/skills/plungeai-discovery/references/list-and-contract). ## 2. Search by capability — REST ```bash curl -s "https://api.plungeai.com/v1/discovery/search?q=web%20search&kind=agents&limit=2" \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` Or let the platform pick for you: ```bash curl -s -X POST https://api.plungeai.com/v1/discovery/recommend \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"type": "agent", "task": "summarize a long PDF into bullet points"}' ``` Full query params (`mode`, `fields`, `include=quality`, …), response shapes, and the full-card markdown route (`GET /v1/discovery/cards/{type}/{id}`) — [`references/search-and-recommend.md`](/skills/plungeai-discovery/references/search-and-recommend). ## 3. Inspect before you call — the tool contract Before the first call to an unfamiliar structured tool-agent (a card with a Parameters table), fetch its invocation contract — it IS the API docs, and (over MCP) carries the acting user's live credential status: ```json plungeai_get_tool_contract {agent_id: "markitdown"} ``` ```bash curl -s https://api.plungeai.com/v1/tools/markitdown -H "Authorization: Bearer ozk_YOUR_KEY" ``` ## 4. Starting points instead of from scratch — templates ```json plungeai_templates {action: "list", category: "research"} plungeai_templates {action: "get", template_id: "<id>"} ``` Browses Studio's own template gallery (ready-made workflows/agents/bots). Full detail — [`references/list-and-contract.md`](/skills/plungeai-discovery/references/list-and-contract). ## Over the One API — every discovery route Same `ozk_` key (`plungeai-api-setup`). Nothing here runs or spends anything; the two `POST` routes `recommend` and `resolve` are rate-limited (`429 rate_limited`, `Retry-After`), the catalog `GET`s are not. | Job | REST | MCP | CLI `ocean-cli …` | |---|---|---|---| | search | `GET /v1/discovery/search?q=&kind=&category=&limit=&offset=&mode=&fields=` → `{cards, count, total, searchMethod}` | `plungeai_list_agents {search, kind, category}` | `discovery search Q --kind` | | one card | `GET /v1/discovery/cards/{type}/{id}` → markdown | `plungeai_list_agents {agent_id}` | `discovery card TYPE ID` | | best card | `POST /v1/discovery/recommend {type, task}` → `{recommended, card, score}` | — | `discovery recommend TASK` | | one answer | `POST /v1/discovery/resolve {task, kinds?, category?}` → `{outcome, tool, operation, call: {required_params, example_cnl}, confidence, alternatives}` | — | `discovery search` / `recommend` | | contract | `GET /v1/tools/{id}` | `plungeai_get_tool_contract` | `tools get ID` | | templates | `GET /v1/templates?kind=&category=&limit=` → `{templates, count}`; `GET /v1/templates/{id}` → `{template, yaml}`; `POST /v1/templates/{id}/use {name?}` → `201 {workflow}` | `plungeai_templates` `list` / `get` / `use` | `templates list` / `get` / `use` | What builders get wrong: - **Page with `total`, not `count`.** `count` is this page's length; search also returns `total` (every card the filters match). `GET /v1/agents` and `GET /v1/tools` have only `count`: page until a short page. - **REST knows more kinds than MCP.** REST `kind`: `agents`, `skills`, `digital-twins`, `personas`, `experts`, `backgrounds`, `workflows`, `models`, `providers`, `connectors`, `plugins`, `mcp-servers`; the MCP tool's `kind` stops at `agents`, `personas`, `experts`, `skills`, `models`, `workflows`, `connectors`. A card URL takes the singular type (`agent`, `digital_twin`, `mcp_server`, …); the plural kinds are accepted as aliases. - **`recommended` can be `null` (score `0`).** On `resolve`, act only on `outcome: "match"` with a real `confidence` (`null` is the keyword fallback); `uncertain` and `none` mean read the cards yourself. - **Agent cards carry `credential`** (`{tier: "platform"}` or `{tier: "connection", provider, method}`): read it before you call, so a connection-tier agent does not surprise you with a `424` (`plungeai-tools-connectors`). - **A template `use` is a copy that is yours.** It creates a saved workflow of the template's kind and runs the same save-time lint (`422 invalid_workflow`, nothing created); `404 template_not_found` for an inactive id. - Model cards read `provider:model`; the model routes take the `provider/model` slug from `GET /v1/models` (`plungeai-models`). - **MCP `format: "json"` is not typed rows.** On `plungeai_list_agents` it wraps the rendered markdown as `{content: "<markdown>"}`; for machine-readable cards use `GET /v1/discovery/search` (or `ocean-cli discovery search --json`). ## Gotchas - **Substring near-misses are not the same id.** `brave-agent` may exist while `brave-search` does not — take ids character-for-character from a result, never guess a variant. - **`count` is a page length, not a catalog total.** Page until a short page comes back (search also answers `total`); don't stop at `offset >= count`. - **Errors on `/v1/discovery/recommend` and `/v1/discovery/cards/{type}/{id}` are always the One API envelope.** The router normalizes the registry's own flat `{"error": "<string>"}` rejections into `{"error":{"code","message"}}` before they reach you (`code` is `not_found` on a 404, `invalid_request` otherwise; the original text lands in `message`) — `err.error.code` is always safe to read on these two routes. - **"My agents" is not the registry.** Users mean their saved workflows (`plungeai_list_workflows`) — the registry (`plungeai_list_agents`) is the platform's own capability catalog, for when YOU are composing. - **Discovery finds the contract; it doesn't execute anything.** Fetching a card or a contract never runs a workflow or spends a token — it's always safe to over-discover. ## Related skills - `plungeai-agents` — execute a prompt-driven agent once you've found it. - `plungeai-tools-connectors` — typed execution against a fetched contract, connected-account status. - `plungeai-models` — the model catalog (`GET /v1/models`) and money plane. - `plungeai-workflows` — instantiate a template into a saved workflow (`plungeai_templates {action: "use"}`), CNL authoring. - `plungeai-results-traces` — execution trace lookups (`GET /v1/traces/{id}`), a separate observability surface. - `plungeai-platform` — how discovery fits into the platform's five layers. ## Reference pages <CardGroup cols={2}> <Card title="Discovery over MCP — list_agents, get_tool_contract, templates (listing)" icon="file-text" href="/skills/plungeai-discovery/references/list-and-contract"> The registry is LIVE: agents come from the registry service at request time, no static manifest. </Card> <Card title="Discovery — /v1/discovery" icon="file-text" href="/skills/plungeai-discovery/references/search-and-recommend"> Discovery is the live catalog of everything the platform can do — agents, workflows, MCP servers, connectors, models — with hybrid semantic + keyword search… </Card> </CardGroup> # Discovery over MCP — list_agents, get_tool_contract, templates (listing) Source: https://www.plungeai.com/docs/skills/plungeai-discovery/references/list-and-contract <!-- sources-of-truth: orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/server.ts, orchestration/mcp-gateway/tool-exec.ts, orchestration/mcp-gateway/registry-cards.ts, orchestration/mcp-gateway/tool-outcome.ts, orchestration/mcp-gateway/extras.ts | last-synced: 2026-09-24 (list/contract half of the original discovery-and-execution.md, plus the plungeai_templates listing actions; the execute_agent/get_result half lives in plungeai-agents, the execute_tool half lives in plungeai-tools-connectors. Re-verified: ListAgentsSchema kind enum + boundedLimit against tools.ts, GetToolContractSchema against tool-exec.ts — all match, no drift found) --> The registry is LIVE: agents come from the registry service at request time, no static manifest. Only `status:active` agents are listed and callable over MCP; the same fence sits on the execution path, so an id you did not just get from a live search is likely to be refused. Take ids from search results, verbatim, every session. --- ## plungeai_list_agents **Purpose:** discover platform building-block agents from the live registry. NOT the user's saved workflows (that is `plungeai_list_workflows`). Read-only. **Parameters** | Param | Type | Notes | |---|---|---| | `search` | string ≤2000 | Hybrid semantic + keyword query over live agent cards. Describe the capability in natural language ("web search", "convert pdf to markdown", "post a message to a channel") and trust the ranking. | | `category` | string | Registry category filter. | | `agent_id` | string | Fetch ONE full card (markdown: operations, parameters, good-at examples, "Not for → use X instead" redirects). | | `kind` | enum | `agents` (default) \| `personas` \| `experts` \| `skills` \| `models` \| `workflows` \| `connectors`. Only `agents` is filtered active-only (the other kinds do not carry the tag). | | `limit` | int 1–100 | Default 25. | | `format` | `markdown` \| `json` | Response serialization; default markdown. | Every call also takes `user_request` (the user's ask, verbatim — every `plungeai_*` tool does; the platform uses it for routing and diagnostics). **Example** ```json {"user_request": "which agents can read SEC filings?", "search": "financial analysis and SEC filings", "limit": 5} ``` **Returns:** the registry's own markdown — ranked cards for a search, a full card for `agent_id`, the whole catalog (large) when unfiltered. Prefer `search`/`category` over an unfiltered listing. **Failures & fixes** - Unknown `agent_id` → "No registry card with id ... exists" plus a *Closest live matches* section. Not an error state — pick a listed live id or refine `search`. - Non-active `agent_id` → the card is withheld ("only `status:active` agents are listed and callable here") plus live alternatives. Same fix. - "Registry unavailable — try again shortly." → transient; retry once after a pause. **Rules:** never quote the catalog size as fixed; before using an unfamiliar agent, fetch its full card — cards carry parameters and redirects that prevent wrong-agent calls. --- ## plungeai_get_tool_contract **Purpose:** the exact invocation contract for one registry agent, generated live from its card: JSON Schema for parameters, the operation list (with approval-gated operations flagged), worked YAML examples, output shape, and LIVE per-provider credential status for the acting user. Fetch it before the first `plungeai_execute_tool` call to an unfamiliar agent — the contract IS the API docs. Read-only. **Parameters:** `agent_id` (required, string), `format` (`markdown` \| `json`, optional). **Example** ```json {"user_request": "what's the weather in Lisbon this weekend?", "agent_id": "weather-agent"} ``` (`weather-agent` here came from a live search in the same conversation — always derive the id from step 1's results, never from this page.) **Returns:** contract markdown + the same contract machine-readable at `structuredContent.contract` (one field of the ok envelope). The Credentials section is decisive: - `🔐 platform-managed` — nothing for the user to connect; just call. - `✅ connected as <email>` — the user's account is linked; call away. - `⚠️ NOT connected` / `expired — reconnect` — a required connection is missing: execution will stop with connect instructions until the user connects it (in a PlungeAI app → Connectors). Relay that BEFORE executing. The footer spells the run call: `plungeai_execute_tool {agent_id, operation: "<one of the operations>", params: {...}}`. Full typed-execution detail (params, outcome envelope, fences) lives in the `plungeai-tools-connectors` skill. **Failures & fixes:** unknown/inactive id → `unavailable` + alternatives (same fence as execution); "The registry did not answer" → `error`, retry in a few seconds. --- ## plungeai_templates — listing saved workflow templates **Purpose:** browse the platform's gallery of ready-made workflow/agent/bot templates (Studio's own template gallery — same `templates`/`template_versions` D1 rows and KV content, so usage counts stay consistent). This is discovery of **pre-built starting points**, distinct from `plungeai_list_agents` (registry building blocks) and `plungeai_list_workflows` (the user's own saved workflows). **Parameters** (listing actions only — `action: "use"` instantiates a template into a saved workflow and belongs to workflow authoring, see `plungeai-workflows`): | Param | Type | Notes | |---|---|---| | `action` | `"list"` \| `"get"` | `list` — the gallery; `get` — one template's full YAML | | `template_id` | string | Required for `get` (and for `use`) | | `category` | string | Filter for `list` | | `limit` | int 1–100 | `list` only | ```json {"user_request": "what workflow templates are available for research?", "action": "list", "category": "research", "limit": 10} ``` `list` returns a markdown table: Name, Kind, Category, Difficulty, Used (usage count), ID, Description — only `status:active` templates. `get` returns the template's name/description/category/difficulty plus its full YAML in a fenced block — read this before deciding whether to instantiate it with `action: "use"` (`plungeai-workflows`). **Failures & fixes:** `get`/`use` without `template_id` → "template_id is required for this action." — supply the id from a `list` call. Unknown `template_id` → "Template not found: <id>" — re-list. --- ## Putting it together — discover before you build ``` 1. plungeai_list_agents {search: "convert pdf to markdown"} → markitdown 2. plungeai_get_tool_contract {agent_id: "markitdown"} → schema; credentials 🔐 platform-managed 3. plungeai_templates {action: "list", category: "documents"} → any ready-made pipeline to start from instead? ``` Skip step 2 only for agents whose contract you fetched earlier in the same conversation. Execution (`plungeai_execute_agent`, `plungeai_execute_tool`, `plungeai_get_result`) is covered in `plungeai-agents` and `plungeai-tools-connectors`. # Discovery — /v1/discovery Source: https://www.plungeai.com/docs/skills/plungeai-discovery/references/search-and-recommend <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/discovery.ts, orchestration/api-gateway/routes/templates.ts, orchestration/api-gateway/docs/guide-3.0/08-discovery.md | last-synced: 2026-10-05 (One API 2.3.0: search `total`, `credential` on agent cards, templates routes, resolve; earlier: discovery half of the original discovery-and-traces.md; the traces half lives in plungeai-results-traces. Re-verified against routes/discovery.ts: SEARCH_PARAMS whitelist, status defaults to active, 4xx/5xx normalization in forward(), KIND_TO_TYPE aliasing — all match, no drift found) --> Discovery is the **live catalog** of everything the platform can do — agents, workflows, MCP servers, connectors, models — with hybrid semantic + keyword search ranked server-side. Auth: `Authorization: Bearer ozk_YOUR_KEY` on every route here. ## Why discovery first Ids, capabilities, and counts change without notice. Any code (especially generated code) that names an agent, tool, MCP server, or model must have taken that id from a live discovery/list response — never from memory, docs, or this skill. Describe the capability in plain words and trust the ranking. ## GET /v1/discovery/search | Param | Values | Notes | |---|---|---| | `q` | free text | Natural-language capability query ("convert pdf to markdown") | | `kind` | `agents` \| `skills` \| `digital-twins` \| `personas` \| `experts` \| `backgrounds` \| `workflows` \| `models` \| `providers` \| `connectors` \| `plugins` \| `mcp-servers` | Catalog segment (plural) | | `type`, `category`, `tier` | strings | Additional filters | | `status` | default `active` | Leave defaulted — active means deployed and callable | | `limit`, `offset` | integers | Paging | | `mode` | `hybrid` \| `keyword` \| `vector` | Force one search leg (default hybrid). Response `searchMethod` values are `hybrid`/`vector`/`text` — keyword mode (and any keyword fallback) answers `"text"`, never `"keyword"` | | `fields` | `list` \| `summary` \| `full` | Card detail level | | `include` | `quality` | Attach measured eval scores per card | ```bash curl -s "https://api.plungeai.com/v1/discovery/search?q=web%20search&kind=agents&limit=2" \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "cards": [ { "id": "brave-agent", "name": "Brave Search", "type": "agent", "category": "search", "status": "active", "description": "Web search via Brave", "tags": ["search", "web"] }, { "id": "exa-agent", "name": "Exa Search", "type": "agent", "category": "search", "status": "active", "description": "Semantic web search", "tags": ["search"] } ], "count": 2, "total": 14, "searchMethod": "hybrid" } ``` The live response also carries a `query` echo object, and extra fields may appear — parse what you need, don't assert the exact shape. `count` is the returned page's length; `total` is every card the filters match, ignoring `limit`/`offset`. Agent cards also carry `credential` (`{"tier": "platform"}` or `{"tier": "connection", "provider", "method"}`). With `include=quality`, each card gains a `quality` block — `{"score": 1, "success_rate": 1, "runs": 12}` — or `null` when unmeasured (no eval runs yet on that card/tier). Treat `null` as "unknown", not "bad". `401` → missing/invalid `ozk_` key. ## POST /v1/discovery/recommend Ask the platform to pick the best card for a task instead of ranking yourself. Body: `{"type": "<card type, e.g. agent>", "task": "<what you need done>"}`. ```bash curl -s -X POST https://api.plungeai.com/v1/discovery/recommend \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"type": "agent", "task": "summarize a long PDF into bullet points"}' ``` ```json { "recommended": "pdf-agent", "card": { "id": "pdf-agent", "name": "PDF Agent", "type": "agent", "category": "documents", "status": "active", "description": "Convert and extract PDFs" }, "score": 0.91 } ``` `recommended` (and `card`) can be **`null`** when nothing matches — handle it; a card does not always come back. Errors — always the One API envelope (`{"error": {"code", "message", ...}}`), but the `code` differs by where the rejection happened: - Unparseable body → `400 {"error":{"code":"invalid_json","message":"JSON body required: { type, task }"}}`. - A parseable body the registry rejects → normalized to `400 {"error":{"code":"invalid_request","message":"Invalid type: x"}}` / `{"code":"invalid_request","message":"Task description required"}` — the registry's own flat `{"error": "<string>"}` is rewritten into the envelope before it reaches you (`message` carries the original text verbatim); it never reaches the caller as a bare string. Always safe to read `err.error.code`. - `429` → `rate_limited` (`Retry-After` header). - `5xx` from the registry never leaks through raw — it becomes a clean `502 {"error":{"code":"upstream_error", ...}}`. ## POST /v1/discovery/resolve Resolve one plain-language task to ONE tool, its operation and an example call, with a calibrated confidence — the shortcut when you want a single answer, not a ranked list. Body: `task` (required), optional `kinds` (≤ 8 card types; default every discoverable kind), `category`, `status` (default `active`). ```bash curl -s -X POST https://api.plungeai.com/v1/discovery/resolve \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"task": "convert a PDF to markdown", "kinds": ["agent"]}' ``` Answer: `{outcome: "match" | "uncertain" | "none", tool: {id, kind, type, name, description, category, status}, operation: {name, purpose, gated}, call: {required_params, example_cnl}, confidence, alternatives: [{id, kind, name, p}], mode}`. `confidence` is `null` on the keyword fallback (the decision model was unavailable). Act on `match` only; on `uncertain` or `none` fall back to `GET /v1/discovery/search` and read the cards. A body with no `task` is `400 invalid_json`; `429 rate_limited` carries `Retry-After`. ## GET /v1/discovery/cards/{type}/{id} The full capability card as **markdown** (`text/markdown`) — the LLM-friendly view with operations, parameters, good-at examples, and "Not for → use X instead" redirects. Fetch it before building on an unfamiliar capability. `{type}` is the singular card type (`agent`, `skill`, `digital_twin`, `expert`, `background`, `workflow`, `model`, `provider`, `connector`, `plugin`, `mcp_server`) — the plural search `kind` values are accepted aliases. ```bash curl -s https://api.plungeai.com/v1/discovery/cards/agent/brave-agent \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```markdown # Brave Search (brave-agent) Web search via the Brave Search API. ## Operations - search — Run a web search (params: query, count?) ... ``` `404` when no such card exists — normalized to the One API envelope, `{"error":{"code":"not_found","message":"Card not found"}}`; a bad `{type}` is `400 {"error":{"code":"invalid_request","message":"Invalid type: …"}}`. The registry's own flat `{"error": "<string>"}` response is rewritten into this envelope (the original text lands in `message`) before it ever reaches you — same normalization as `/v1/discovery/recommend` above. ## Templates — `/v1/templates` Starter workflows, agents and bots (the gallery behind `plungeai_templates` and Studio's template picker). ```bash curl -s "https://api.plungeai.com/v1/templates?kind=bot&limit=3" -H "Authorization: Bearer ozk_YOUR_KEY" # 200 {"templates":[{"id","name","kind","category","difficulty","usage_count","description"}],"count":3} curl -s https://api.plungeai.com/v1/templates/<template id> -H "Authorization: Bearer ozk_YOUR_KEY" # 200 {"template": {…}, "yaml": "…"} curl -s -X POST https://api.plungeai.com/v1/templates/<template id>/use \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" -d '{"name": "My copy"}' # 201 {"workflow": {…}} a saved workflow of the template's kind ``` `kind` is `workflow`, `agent` or `bot` (else `422 invalid_params`); `limit` 1–100, default 50; `category` filters. Active templates only, featured first. `use` keeps a bot template's own name and names the others `<template> (copy)` unless you pass `name` (1–512 characters). The copy runs the same save-time lint as `POST /v1/workflows`: `422 invalid_workflow` (with `errors` and `warnings`) creates nothing; `404 template_not_found` for an id that is not an active template. CLI: `ocean-cli templates list | get | use`. ## Related Execution trace lookups (`GET /v1/traces/{id}`) are a separate observability surface — see the `plungeai-results-traces` skill, not this one. # plungeai-memory Source: https://www.plungeai.com/docs/skills/plungeai-memory Read and write PlungeAI's per-user long-term memory over MCP (plungeai_memory: recall/remember/search_runs/get_run), the CLI (`ocean-cli memory`) and the One API (`/v1/memory/recall`, `/remember`, `/forget`, `/runs`), and save or distill a skill from a session (plungeai_learn, `POST /v1/skills/learn`) — distinct from SharedMemory (a single run's output). Use when the user says remember this, asks what the agent knows about them, wants to review past runs, or wants findings saved as a skill for future runs. For a single run's output use `plungeai-results-traces`; for the skill/expert/persona/background/plugin capability system a saved skill feeds into use `plungeai-skills-plugins`. [Download zip](https://skills.plungeai.com/plungeai-memory.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-memory/SKILL.md) Two separate systems, never confuse them: **SharedMemory** holds one execution's task outputs (read with `plungeai_get_result`); **long-term memory** holds what the platform has learned about the user across every run, recalled automatically at mission start and written back with `plungeai_memory` or the agent's in-loop `memory` tool. ## Prerequisites - A self-service `ozk_` API key from **Dashboard → One API → Keys** (https://dashboard.plungeai.com), or an MCP client connected to `https://mcp.plungeai.com/v1`. - No discovery step needed for the memory tools themselves — they operate on the caller's own store. Discover agents you plan to reference (e.g. in `learn`) first: `plungeai_list_agents`. ## Reading and writing (MCP) ``` plungeai_memory {action: "recall"} # read the durable store plungeai_memory {action: "remember", target: "user", # user | memory operation: "add", content: "Prefers weekly summaries, no emojis"} plungeai_memory {action: "search_runs", query: "competitor analysis"} plungeai_memory {action: "get_run", run_id: "…"} ``` `recall` returns the same MEMORY.md + USER.md snapshot a mission gets automatically at launch. Use `remember` any time the user says "remember this" — chat context alone does not persist. `replace`/`remove` need the exact `old_text`; a failed attempt returns the current entries so you can copy it verbatim and retry. ## Distilling a session into a skill — `plungeai_learn` ``` plungeai_learn {source: "<a URL, pasted text, or distilled findings from this chat>"} plungeai_learn {action: "list"} # your learned skills plungeai_learn {action: "forget", name: "<skill id>"} # delete one you own ``` `action` defaults to `learn` (async: poll `plungeai_get_workflow_status`, fetch with `plungeai_get_result`). Offer this "learn-back" move whenever a session produced real research worth reusing — the saved skill is private to the caller and can be declared on future mission/harness tasks (`plungeai-skills-plugins`). ## Over the One API Same store, same `ozk_` key (`Authorization: Bearer`; setup: `plungeai-api-setup`). Memory belongs to the key's user, so another user's entry or run id answers the same `404` as a missing one. | Job | REST | MCP `plungeai_memory` | CLI `ocean-cli memory …` | |---|---|---|---| | read entries | `GET /v1/memory/recall?q=&limit=` (1–100, default 50) → `{entries: [{id, scope, text}], count}` | `recall` (the whole snapshot as text) | `recall [query]` (query is advisory) | | write | `POST /v1/memory/remember {text, scope?}` → `201`, or `200` if that text is already stored | `remember` `{target, operation, content}` | `remember --add \| --replace OLD NEW \| --remove` | | delete | `POST /v1/memory/forget {id}` or `{text}` (exactly one) → `{removed}` | `remember` `operation: remove` + `old_text` | `remember --remove TEXT` | | past runs | `GET /v1/memory/runs?q=` (`q` required, limit ≤ 100, default 10) → `{runs: [{run_id, ts, goal, snippet}], count}`; `GET /v1/memory/runs/{id}` → `{run_id, content}` | `search_runs` / `get_run` | `search-runs QUERY` / `get-run ID` | | skills | `GET /v1/skills` → `{skills: [{id, description}], count}`; `POST /v1/skills/learn {name, body, description?}` → `201 {skill}`; `DELETE /v1/skills/{id}` → `204` | `plungeai_learn` `list` / `learn` / `forget` | `learn list` / `add` / `forget` | What builders get wrong: - **Same store, different spellings.** REST says `scope`/`text`, MCP says `target`/`content`. Both default to the `memory` store; the CLI's `--target` defaults to `user`, so pass it when it matters. - **An entry id is `mem_` + 12 hex of its scope and text.** `forget {id}` is `404 memory_not_found` if your stores do not hold it and `409 memory_conflict` when that text is also part of another entry — then `forget {text}`, which purges every entry containing it from both stores. - **`422 memory_rejected`** (store full, or the text matched a threat pattern) carries `current_entries`: consolidate, then resend. `422 invalid_params`: `text` is 1–65,536 characters, `scope` is `memory` or `user`. - **`learn` means two things.** `POST /v1/skills/learn` saves markdown you already wrote (`name` kebab-case, 2–64 characters; `description` ≤ 60; an existing name of yours is replaced; `409 skill_name_taken` for a platform or another user's name; `502 registry_unavailable` is not an empty list). Having an agent distil a skill from a URL or text is `plungeai_learn` / `ocean-cli learn add`. - Every route takes `format` (`json` default, `yaml`, `markdown`, `text`); `plungeai_memory` is plain text. ## Gotchas - **Recall is a frozen snapshot** taken at run start — mid-run writes are durable but do not change the running prompt; two missions launched together never see each other's writes. - `episodes.md` (the append-only run log) is never injected into recall — pull run history explicitly with `search_runs` / `get_run`. - Memory is small and curated on purpose (MEMORY ~2200 chars, USER ~1375 default, clamped 500–20000) — write a few load-bearing facts, not a dump; every entry is threat-scanned twice (rejected at write, `[BLOCKED]`-replaced at snapshot build if it slipped through). - Bots do **not** set `memory_owner` (policy since 2026-08-23 — omit it entirely). Saving a workflow, agent, bot or campaign whose YAML carries `memory_owner:` is refused (`422 invalid_workflow`); a bot run reads/writes the owner's general memory namespace, shared across all their bots and runs. See `orchestration/BOT-CREATION.md`. - Delegate children never write memory (the `memory` tool is stripped) — one writer per run. - Standing company/product context that should apply to every run is NOT memory — it is a **background** card; a reusable method is a **skill** — see `plungeai-skills-plugins`. ## Related skills - `plungeai-results-traces` — SharedMemory / `plungeai_get_result`, a single run's output. - `plungeai-missions` — the mission lifecycle memory recalls into and writes from. - `plungeai-scheduling` — which scheduled job types keep the memory lifecycle. - `plungeai-skills-plugins` — declaring the skill `plungeai_learn` just saved. ## Reference - [`references/memory.md`](/skills/plungeai-memory/references/memory) — full SharedMemory-vs-long-term-memory comparison, the three-layer store (USER/MEMORY/episodes), budgets, safety scanning, namespaces, and the `plungeai_memory` / `plungeai_learn` MCP tool contracts. ## Reference pages <CardGroup cols={2}> <Card title="Memory — run data (SharedMemory) vs long-term memory. Two systems, never confuse them" icon="file-text" href="/skills/plungeai-memory/references/memory"> PlungeAI has TWO memory systems with different jobs: </Card> </CardGroup> # Memory — run data (SharedMemory) vs long-term memory. Two systems, never confuse them Source: https://www.plungeai.com/docs/skills/plungeai-memory/references/memory <!-- sources-of-truth: core/core-memory/core-memory.ts, CLAUDE.md (SharedMemory usage), orchestration/BOT-CREATION.md (memory_owner policy), orchestration/mcp-gateway/server.ts, orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/extras.ts, orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/memory.ts, orchestration/api-gateway/routes/skills.ts, orchestration/mcp-gateway/ops/memory.ts, orchestration/mcp-gateway/ops/learn.ts, orchestration/cnl-engine/schema-types.ts | last-synced: 2026-10-05 (One API 2.3.0: added the /v1/memory and /v1/skills routes) --> PlungeAI has TWO memory systems with different jobs: | | **SharedMemory** | **Long-term memory** | |---|---|---| | Holds | Every task's output within/after a run | What the agent LEARNED about the user and its work | | Keyed by | `(workflow_id, task_id, user)` | Per user (or per bot namespace) | | Lifetime | Per execution, durable for retrieval | Standing, across all runs | | You read it via | `plungeai_get_result`, `/v1/*/results/…` | Automatic recall in runs; `plungeai_memory` | | You write it via | Never directly — agents store results | `plungeai_memory remember`; the agent's own `memory` tool | "Fetch what that run produced" is SharedMemory. "Remember that the user prefers weekly summaries" is long-term memory. ## SharedMemory — the run-data fabric Every task result — including single agent calls — is stored under `(workflow_id, task_id)` scoped to your user. That is why every execution acknowledgement returns those ids: they are redemption tickets. ``` # MCP — final result (full conversation thread for conversational runs) plungeai_get_result {execution_id: "exec-…"} # One step of a fan-out plungeai_get_result {execution_id: "exec-…", task_id: "web"} ``` ```bash # One API curl -s https://api.plungeai.com/v1/workflows/results/{workflowId}/{taskId} \ -H "Authorization: Bearer ozk_YOUR_KEY" curl -s https://api.plungeai.com/v1/agents/results/{workflowId}/{taskId} \ -H "Authorization: Bearer ozk_YOUR_KEY" # 200 {content, content_type, workflow_id, task_id} | 404 not_ready ``` Facts to rely on: - `404 not_ready` = still running (or a beat behind) — poll; not an error. - Results are scoped to the executing user — you cannot redeem another user's ids. - The final result of a run lists its steps (MCP renders them); open any step in full with its `task_id`. Use this to inspect ONE branch of a parallel block instead of re-running. - Within a workflow, data handoff between tasks IS SharedMemory — automatic; never hand-wire results between tasks in YAML. - A scheduler run id is not a SharedMemory id: go through `plungeai_schedule {action: "runs"}` and use its Execution ID column (`plungeai-scheduling`). ## Long-term memory — the agent's durable brain Per user, three layers of plain markdown: | Layer | File | What it holds | |---|---|---| | L3 | `USER.md` | The agent's learned model of the user (preferences, context) | | L2 | `MEMORY.md` | Curated agent notes shared across this user's missions | | L1 | `episodes.md` | Append-only run log: goal + self-reported outcome per mission | ### Recall — a frozen snapshot per run At mission start the platform builds ONE snapshot of the **curated stores only — MEMORY.md + USER.md. `episodes.md` is never injected** (task outcomes go stale; pull run history explicitly with `search_runs` or the mission's `recall` tool). The snapshot is prepended to the agent's system prompt. Mid-run writes are durable but do **not** mutate the running prompt (prompt-prefix stays cache-stable). So: what a mission "knows" is fixed at launch; what it learns benefits the NEXT run. Two missions launched together do not see each other's writes. ### Writing - **In-loop:** the agent's `memory` tool (missions write learnings as they work; episodes append automatically at run end). - **From outside:** `plungeai_memory` (MCP), `ocean-cli memory`, or the One API (next section): ``` plungeai_memory {action: "recall"} # read the durable store plungeai_memory {action: "remember", target: "user", # user | memory operation: "add", # add | replace | remove | read content: "Prefers weekly summaries, no emojis in reports"} plungeai_memory {action: "search_runs", query: "competitor analysis"} plungeai_memory {action: "get_run", run_id: "…"} ``` Use `remember` whenever the user says "remember this". Note the split: `recall` reads the curated stores; the **run journal** (`search_runs`/`get_run`) is separate and deliberately NOT part of recall — history is queried, not ambient. ### Budgets — memory is curated, not a dump The stores are small on purpose (defaults: MEMORY ~2200 chars, USER ~1375; configurable per user, clamped to 500–20000). Writes beyond the limit force curation — replace/remove stale entries rather than appending forever. Write memory like an engineer writes a runbook: few, load-bearing, current facts. Ten vague notes crowd out the one that matters. ### Safety Every entry is threat-scanned twice: at write (a prompt-injection-looking entry is **rejected**) and at snapshot build (a poisoned stored entry is replaced with a `[BLOCKED]` placeholder; the raw text stays inspectable by the user). Do not try to store instructions-to-future-agents phrased as commands — that is exactly the pattern the scanner exists to stop. Store facts. ## Over the One API — `/v1/memory`, `/v1/skills` Auth: `Authorization: Bearer ozk_YOUR_KEY`. Every call acts on the key's own user; `format` (`json` default, `yaml`, `markdown`, `text`) is accepted in the body or as `?format=`. ```bash curl -s "https://api.plungeai.com/v1/memory/recall?q=weekly&limit=20" -H "Authorization: Bearer ozk_YOUR_KEY" # 200 {"entries":[{"id":"mem_3f2a9c01b7de","scope":"user","text":"Prefers weekly summaries"}],"count":1} curl -s -X POST https://api.plungeai.com/v1/memory/remember -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" -d '{"text":"Prefers weekly summaries, no emojis","scope":"user"}' # 201 {"id":"mem_…","scope":"user","text":"…","usage":"…","message":"added"} (200 when already stored) curl -s -X POST https://api.plungeai.com/v1/memory/forget -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" -d '{"id":"mem_3f2a9c01b7de"}' # or {"text":"weekly"} # 200 {"removed":1} ``` | Route | Notes | |---|---| | `GET /v1/memory/recall` | `q` keeps entries containing it (case-insensitive) across the MEMORY and USER stores; `limit` 1–100, default 50 | | `POST /v1/memory/remember` | `text` 1–65,536 chars; `scope` `memory` (default) or `user`; `422 memory_rejected` carries `current_entries` | | `POST /v1/memory/forget` | exactly one of `id` / `text` (else `422 invalid_params`); `404 memory_not_found`; `409 memory_conflict` | | `GET /v1/memory/runs` | `q` required (`422 invalid_params`); hits are `{run_id, ts, goal, snippet}` | | `GET /v1/memory/runs/{id}` | `{run_id, content}`; `404 run_not_found`, also for another user's run | | `GET /v1/skills`, `POST /v1/skills/learn`, `DELETE /v1/skills/{id}` | your private skills only (`{skills: [{id, description}], count}`); `learn` saves the markdown `body` you send under `name` — it does not distil a source | An entry's `id` is `mem_` plus twelve hex characters derived from its scope and text, so it is stable between reads and cannot name an entry your stores do not hold. REST `recall` returns structured entries; MCP `recall` returns the MEMORY.md + USER.md snapshot a mission starts with. ## Namespaces and special runs - **Bots:** policy since 2026-08-23 is to **omit `memory_owner` entirely** (a saved workflow/agent/bot/campaign YAML carrying it is refused, `422 invalid_workflow`) — the first 20 bots were authored with a hardcoded `memory_owner: "<ownerUserId>:bot:<workflowId>"` and all had it stripped. Without it, a bot run reads/writes the **owner's general memory namespace**, shared across all their bots and runs ("check memory" / "store in memory" phrasing still works; prompts are a bit larger since the recall snapshot is built from the whole store). The harness guard (`resolveMemoryUser`) honors a hardcoded `memory_owner` only when it names the runner's own identity — anything else is rejected with a warning and falls back to the owner's store, so a copied YAML can never read or write another user's memory even if it tries. Full policy and rationale: `orchestration/BOT-CREATION.md`. (Do not confuse this with a workflow's `followup.memory_scope: all_tasks|last_task`, which only scopes follow-up chat context.) - **Scheduled runs** keep the full per-user memory lifecycle — a daily mission genuinely accumulates. Only system jobs and heartbeat condition checks are memory-free (heartbeat is a user-facing job type — see `plungeai-scheduling` — whose ticks are watch-and-notify checks, not learning runs). - **Delegate children** never write memory: the `memory` tool is stripped from fan-out children by design — one writer per run. ## Choosing the right memory move | Need | Move | |---|---| | Output of a run/step | `plungeai_get_result` (+ `task_id`) | | Pass data between workflow steps | Nothing — automatic | | "Remember X about me/us" | `plungeai_memory remember` | | Mission should apply past learnings | Automatic recall — just run it; curate memory if recall is noisy | | "What did we do about X before?" | `plungeai_memory search_runs` → `get_run` | | Standing company/product context for many runs | Not memory — a **background** card (`plungeai-skills-plugins`) | | Reusable method/knowledge pack | Not memory — a **skill**, created with `plungeai_learn` (below) | The last two rows matter: memory is per-user learned state. Shared, versioned, deliberately-authored context belongs in backgrounds and skills, where injection is explicit and reviewable. --- ## MCP tool: `plungeai_memory` **Purpose:** the user's long-term memory. Two stores, deliberately separate: the durable memory that `recall` reads (and `remember` writes), and the run journal of past mission runs (`search_runs`/`get_run`). **Actions** | Action | Requires | Notes | |---|---|---| | `recall` | — | Everything the platform remembers about the user (the same recall missions get automatically). | | `remember` | `content` (add/replace) or `old_text` (remove); neither for read | Durable write. `target`: `user` (profile facts) \| `memory` (working knowledge, default). `operation`: `add` (default) \| `replace` (needs `old_text` + `content`) \| `remove` (needs `old_text`) \| `read`. `recall` sees writes immediately. | | `search_runs` | `query` | Search the run journal (optional `limit`, default 10). | | `get_run` | `run_id` | One journal entry in full. | **Example:** `{"user_request": "remember that I prefer summaries under 200 words", "action": "remember", "target": "user", "content": "Prefers summaries under 200 words"}` **Returns:** `recall` → the memory document (or "No memory recorded yet."); `remember` → "Saved to your user profile/memory ... recall will see it immediately."; failed writes return the error plus the CURRENT entries so a `replace`/`remove` can be retried with an exact `old_text`. **Failures & fixes:** "content is required for remember" / "query is required for search_runs" / "run_id is required for get_run" → supply them. A `replace`/`remove` that misses names the current entries — copy `old_text` verbatim from that list and retry. Use `remember` whenever the user says "remember this" — chat context alone does not persist. --- ## MCP tool: `plungeai_learn` **Purpose:** distill a source into a reusable skill, saved PRIVATE to the user — future runs can inject it. The source can be a URL, pasted text/markdown, or a description of what was just accomplished in this chat (learn-back). Runs the platform's `learn` agent card, which resolves live at run time. **Async by default.** **Parameters** | Param | Type | Notes | |---|---|---| | `action` | `learn` (default) \| `list` \| `forget` | `learn`: distill `source` into a skill. `list`: your learned skills. `forget`: delete one (`name` required). | | `source` | string | Required for `action: learn` (a URL, pasted text/markdown to distill, or a description of what you just did). Not used by `list`/`forget`. | | `name` | string (kebab-case) | On `learn`: optional skill id — the agent picks one if omitted. On `forget`: required (the id from `action: list`). | | `mode` | `sync` \| `async` | Only applies to `learn`; default async. | **Example (learn):** `{"user_request": "save what we learned about CNL debugging as a skill", "source": "Distilled findings from this session: ...", "name": "cnl-debugging"}` **Example (list):** `{"user_request": "what skills have I saved?", "action": "list"}` **Example (forget):** `{"user_request": "delete my cnl-debugging skill", "action": "forget", "name": "cnl-debugging"}` **Returns:** - `learn`, async (default): "Started distilling `<id>` (async)..." → poll `plungeai_get_workflow_status`, fetch with `plungeai_get_result` (the result names the saved skill). - `list`: a table of your learned skills (`- \`id\` — description`), or "No learned skills yet." - `forget`: "Forgot skill `<name>`." **Failures & fixes:** `learn` without `source` → `needs_input`. `forget` without `name` → "`forget` needs `name` (the skill id from `action: list`)." `forget` on an id you don't own, or that doesn't exist → "Not yours to delete" / "No skill named `<name>`. Use `action: list` to see yours." Authentication required (needs a real user key); unreachable URLs on `learn` surface in the run result — retry with pasted text instead. This is the "learn-back" move: when a session produced real research, offer to distill it so future runs can inject it as a skill — see `plungeai-skills-plugins` for how a saved skill gets declared and injected on later runs. # plungeai-missions Source: https://www.plungeai.com/docs/skills/plungeai-missions Run bounded autonomous PlungeAI agent missions (type: harness) — a goal, a tool fence, an iteration cap, and self-checked success criteria, via plungeai_run_mission, `ocean-cli missions run`, or a harness task over the One API (`POST /v1/workflows/execute`, the Agent Call door `POST /v1/execute`). Use when the steps to reach a goal are not known in advance, when you need one open-ended researcher/verifier/analyst loop instead of a guessed chain of small tasks, or when the user asks for a bounded agent run. For a fixed pipeline of known steps use `plungeai-workflows`; for cron-scheduled missions use `plungeai-scheduling`; for a list-to-completion campaign ledger use `plungeai-campaigns`. [Download zip](https://skills.plungeai.com/plungeai-missions.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-missions/SKILL.md) A mission gives one agent a `goal`, a standing purpose, a fenced tool set, an iteration cap, and success criteria it self-checks before finishing — then runs a real ReAct loop (think → tool call → result → …) against it, hard-stopped at every bound. Reach for a mission when the steps to reach the goal are NOT known in advance; use a plain workflow (`plungeai-workflows`) when they are. ## Prerequisites - A self-service `ozk_` API key from **Dashboard → One API → Keys** (https://dashboard.plungeai.com), or an MCP client already connected to `https://mcp.plungeai.com/v1`. - Confirm identity first: `plungeai_whoami`. ## Discovery first Never guess a skill, expert, persona, plugin, or MCP server id — a wrong id degrades to a silent warning, not a hard failure. Look ids up live: `plungeai_list_agents {kind: "skills"|"experts"|"personas"|"connectors", search: "<topic>"}` (REST: `GET /v1/discovery/search?kind=…&q=…`). ## Running a mission — the two doors **Quick, from an agent context** — `plungeai_run_mission` accepts a fixed subset of the mission contract: ``` plungeai_run_mission {goal, mission?, allowed_tools?, max_iterations? (≤50), success_criteria?, persona?, skills?, agent?, mode?: "sync"|"async"} ``` ```json {"user_request": "research the EU AI Act's impact on medical devices", "goal": "Produce a sourced brief on how the EU AI Act affects medical-device software vendors", "max_iterations": 12, "success_criteria": ["cites primary sources", "covers timelines and penalties"]} ``` Defaults to async: poll `plungeai_get_workflow_status`, fetch with `plungeai_get_result`. A paused run (`⏸ AWAITING USER APPROVAL` / `AWAITING USER`) is relayed to the user and resumed with `plungeai_continue`. **Full control, as a workflow task** — anything beyond that subset (`experts`, `backgrounds`, `plugins`, `mcp`, `model`/`provider`, `effort`, `max_parallel`, `permissions`, `local`) requires a one-task `type: harness` workflow, run with `plungeai_execute_workflow`: ```yaml name: Claim verification tasks: - type: harness agent: one-agent goal: "Verify the claims in {input} and produce a sourced verdict" mission: | You are a careful researcher. Verify claims against primary sources. Refuse to conclude beyond the evidence. skills: [research] effort: standard allowed_tools: [web_search, web_fetch, task_complete] success_criteria: - Every verdict cites at least one primary source ``` Or reuse a pre-built agent card: `mission_ref: research-analyst` (equivalent shorthand: a single-token `mission: research-analyst`). Cards are also directly schedulable — see `plungeai-scheduling`. ## Over the One API A mission is one `type: harness` task, so REST runs it on the workflows plane. Same `ozk_` key (`plungeai-api-setup`); the YAML above is the program. | Job | REST | MCP | CLI `ocean-cli …` | |---|---|---|---| | run inline | `POST /v1/workflows/execute` (`{workflow, input}` or a `text/yaml` body) → `{success, status, workflow_id, final_task_id, request_id, content?}`, answered when the run is done; `POST /v1/workflows/execute-stream` for SSE | `plungeai_run_mission`; `plungeai_execute_workflow` | `missions run GOAL [--sync]` | | run a saved one | save with `POST /v1/workflows` (`kind: agent`), then `POST /v1/execute {agent: "<id>", input}` (`<id>@3` pins snapshot 3) or `POST /v1/workflows/{id}/execute {input}` | `plungeai_execute_workflow {workflow_id}` | `workflows run ID` | | run a pre-built card | `POST /v1/agents/{id}/execute {prompt, sync?: false}` (the card's id) — the `prompt` is the goal; `sync: false` answers `202` with a pointer | `plungeai_execute_agent` | `agents run ID PROMPT` | | poll, resume | `GET /v1/executions/{id}`, `GET …/{id}/output`, `POST …/{id}/continue`, `POST …/{id}/cancel` | `plungeai_get_workflow_status`, `plungeai_get_result`, `plungeai_continue` | `missions status \| poll \| result ID`, `executions continue ID` | What builders get wrong: - **The workflow routes have no async flag.** `POST /v1/workflows/execute` holds the connection until the mission ends. For a long one use `execute-stream`, a pre-built card with `sync: false`, or fire a saved mission from a schedule (`POST /v1/schedules/{id}/run` → `{run_id, execution_id}` at once), then poll `GET /v1/executions/{id}`. - **`200` is not "done well".** A loop that stops at its turn budget before `task_complete` answers `status: "incomplete"` with a partial result; `completed` is the clean end. A pause reads `needs_approval` or `needs_input` on `GET /v1/executions/{id}` (never an error): answer it with `POST …/continue`. - **A saved mission is linted.** `kind: agent` takes exactly ONE `type: harness` task; a declared `allowed_tools` fence must contain `task_complete`; `memory_owner:` is refused. A failure is `422 invalid_workflow` with `errors` and nothing is written. - **Another account's execution or saved-agent id is a 404** (`execution_not_found`, `agent_not_found`), the same as a missing one. ## The tool fence — the core safety mechanism `allowed_tools` is a hard whitelist: an out-of-fence tool call is refused by the runtime, never just discouraged. Always include `task_complete`. Give the smallest set that can achieve the goal — a verification mission needs `web_search, web_fetch, task_complete`, not the file tools. The loop runtime is `agent: one-agent` — name it on every harness task (left out, the task still lands on a legacy id until the switch-over); its default fence is the thin registry-first one, and an explicit `allowed_tools` always wins over it. Full catalog and the agent fence / money-class protection: [`references/missions.md`](/skills/plungeai-missions/references/missions). ## Gotchas - No per-task `retry` on harness — mission runs are not idempotent; check `plungeai_executions` before re-firing anything with side effects. - `success_criteria` are self-checked by the agent — treat them as guidance-grade, not proof. - Merge order for cards is last-wins per key (card → workflow root → task), and **arrays REPLACE, never union** — a task-level `skills: [x]` replaces the whole card list. - A run pauses (never dies) on `ask_user` questions or `permissions: ask` gates; resume with `plungeai_continue`, never retry around a pause. - Memory: the platform recalls the owner's long-term memory as a frozen snapshot at run start and writes back durably mid-run — see `plungeai-memory`. ## Related skills - `plungeai-workflows` — plain multi-step CNL pipelines; embed a mission as one task. - `plungeai-scheduling` — cron a mission (or a `mission_ref` card) to run on its own. - `plungeai-memory` — the recall/write lifecycle a mission runs against. - `plungeai-skills-plugins` — the `skills`/`experts`/`persona`/`backgrounds`/`plugins`/`mcp` capability fields. - `plungeai-results-traces` — read a mission run's status, output, and trace. - `plungeai-campaigns` — running a list to completion instead of one bounded goal. ## Reference - [`references/missions.md`](/skills/plungeai-missions/references/missions) — full field reference, tool/agent fences, recursion guards, pre-built cards, and the `plungeai_run_mission` MCP tool contract. ## Reference pages <CardGroup cols={2}> <Card title="Missions — bounded autonomous agent runs (type: harness)" icon="file-text" href="/skills/plungeai-missions/references/missions"> A mission bounds what an agent may do for ONE goal: a standing purpose, a fenced tool set, an iteration cap, optional identity/knowledge injection, and… </Card> </CardGroup> # Missions — bounded autonomous agent runs (type: harness) Source: https://www.plungeai.com/docs/skills/plungeai-missions/references/missions <!-- sources-of-truth: orchestration/cnl-engine/harness-mission.ts, orchestration/cnl-engine/schema-types.ts (InlineMission), orchestration/HARNESS-README.md, orchestration/BOT-CREATION.md (memory_owner policy), orchestration/mcp-gateway/server.ts, orchestration/mcp-gateway/tools.ts, orchestration/api-gateway/routes/workflows.ts, orchestration/api-gateway/routes/saved.ts, core/core-base/kind-lint.ts | last-synced: 2026-10-05 (One API 2.3.0: REST run paths, save-time lint; earlier 2026-09-24) --> A **mission** bounds what an agent may do for ONE goal: a standing purpose, a fenced tool set, an iteration cap, optional identity/knowledge injection, and self-checked success criteria. You give a `goal`; the platform runs a real ReAct tool loop (think → tool call → result → …) against it, hard-stopped at every bound. This is the capability to reach for when the steps are NOT known in advance — one harness task, not a chain of guessed small tasks. Missions are humans-and-workflows-in-control by construction: the agent executes *inside* the bounds; it cannot widen them. ## Minimal and typical ```yaml name: Claim verification tasks: - type: harness agent: one-agent goal: "Verify the claims in {input} and produce a sourced verdict" mission: | You are a careful researcher. Verify claims against primary sources. Refuse to conclude beyond the evidence. skills: [research] effort: standard allowed_tools: [web_search, web_fetch, task_complete] success_criteria: - Every verdict cites at least one primary source ``` Or from an agent context, without YAML — but note the MCP tool accepts only a **subset** of the mission contract: ``` plungeai_run_mission {goal, mission?, allowed_tools?, max_iterations? (≤50), success_criteria?, persona?, skills?, agent?, mode?: "sync"|"async"} ``` Those are ALL the fields. Anything beyond them — `experts`, `backgrounds`, `plugins`, `mcp`, `model`/`provider`, `effort`, `max_parallel`, `permissions`, `local` — requires authoring a one-task `type: harness` workflow (the full table below) and running it via `plungeai_execute_workflow`. Defaults to async — poll `plungeai_get_workflow_status`, fetch with `plungeai_get_result`. The run recalls and writes long-term memory automatically (see `plungeai-memory`). If status reports `⏸ AWAITING USER APPROVAL` or `AWAITING USER`, relay it and continue with `plungeai_continue` after the user decides. ## The authoring contract — flat peers, one internal shape Capability fields are written FLAT on the task (the engine folds them into the mission internally). The nested `mission: { … }` object form also works; flat wins per key over nested. ### Full field reference | Field | Type | What it bounds | |---|---|---| | `goal` | string | The per-run input — becomes the agent's prompt | | `mission` | multi-line string | Standing purpose → system-prompt frame. A SINGLE-TOKEN string is a card reference instead (below) | | `persona` | string | One voice per run (`digital_twin` alias; `personas: [x]` → first entry) | | `skills` | string[] | Instruction packs — first 5 eager, rest on demand (`plungeai-skills-plugins`) | | `experts` | string[] | Domain lenses — first 3 eager (`plungeai-skills-plugins`) | | `backgrounds` | string[] | Ambient context cards — first 3 eager, injected first | | `plugins` | string[] | Bundles: skill index + MCP servers + scripts (`plungeai-skills-plugins`) | | `mcp` | string[] | MCP server ids connected in-loop | | `model`, `provider` | string | Model/provider override (`plungeai-models`) | | `effort` | `quick`\|`standard`\|`deep` | Turn-budget preset: **4 / 12 / 20** turns | | `max_turns` | number | Explicit loop cap (alias `max_iterations`); wins over `effort`; runtime default 50 | | `max_tokens` | number | Per-turn OUTPUT token budget for the loop's model calls (runtime default 16384) | | `budget_usd_run` | number | Per-run spend cap in USD — the loop prices its running token cost each turn and stops (`stopReason: "budget"`) once exceeded; absent = no cap | | `max_parallel` | number | Widest fan-out one `delegate` call may spawn — cap-and-refuse, never silently batched | | `allowed_tools` | string[] | The tool fence (below) | | `allowed_agents` | string[] \| `'all'` | Agent fence for `call_agent` | | `denied_agents` | string[] | Subtracted from the agent fence | | `permissions` | map tool → `allow`\|`ask`\|`deny` | Per-tool human gate: `ask` pauses the run with `needs_approval` before the tool executes; `deny` rejects the call. Keys may be tool names or permission classes (`send`\|`write`\|`pay`\|`delete`) | | `permission_locks` | string[] | Admin-locked permission classes: a class listed here forces its `allow` up to `ask`, so a bot author's own `allow` cannot silently auto-run it. Sourced from the DB at the Studio execute boundary, not trusted from raw YAML for enforcement — carried through, not authored | | `success_criteria` | string[] | Self-checked statements the agent verifies before finishing | | `instructions` | string | Extra author instructions, appended after skills — always lands | | `python_executor` | `auto`\|`pyodide`\|`anthropic`\|`gemini` | `run_python` tier routing | | `memory_owner` | string | Memory namespace override — **do not use; omit entirely** (policy since 2026-08-23). Identity does not belong in mission YAML: ownership lives on the workflow row and runtime identity is injected per run; a hardcoded id that isn't the runner's own is rejected by the harness guard anyway, and a saved workflow/agent/bot/campaign YAML carrying it is refused at save time (`422 invalid_workflow`). See `plungeai-memory` and `orchestration/BOT-CREATION.md` | | `local`, `local_agents` | bool, string[] | Local bot: actions route to the user's own machine via the desktop daemon (cloud brain, local hands); requires a connected local node | ## The tool fence `allowed_tools` is a hard whitelist — only listed tools ever reach the LLM; an out-of-fence call is refused by the runtime, not merely discouraged. Omit it and the loop runtime applies its own **fail-closed default fence**. The full loop-tool catalog (fence vocabulary): `read_file`, `write_file`, `edit_file`, `list_files`, `search_files`, `delete_file`, `web_search`, `web_fetch`, `load_skill`, `memory`, `knowledge`, `skill_manage`, `recall`, `recall_history`, `delegate`, `local_agent`, `registry_search`, `registry_lookup`, `call_agent`, `invoke_workflow`, `run_python`, `ask_user`, `platform_action`, `task_complete`. Fence design rules: - **Always include `task_complete`** — it is how a run ends cleanly. - Smallest set that can achieve the goal. A verification mission needs `web_search, web_fetch, task_complete` — not the file tools. - `ask_user` keeps human-question continuations available on every surface; include it when the goal may need clarification mid-run. - The loop runtime is `agent: one-agent` — always name it on the harness task (a harness task with no `agent:` still lands on the legacy `harness-agent` id until the switch-over). Its default fence is thin: the registry triad `registry_search` + `registry_lookup` + `call_agent`, plus `web_search` + `web_fetch` + `load_skill` + `ask_user` + `task_complete` — an unfenced mission CAN call registry agents. An explicit `allowed_tools` always wins over the default. ## The agent fence and money-class protection `call_agent` lets the loop invoke registry agents. `allowed_agents: [a, b]` limits it to those; `'all'` opens the catalog minus `denied_agents`. **Payment/blockchain-class agents stay deny-unless-explicitly-named even under `'all'`** — naming them is the only way a mission can touch money, and `permissions: {call_agent: ask}` adds a human gate on top. This is a trust fence: surface refusals, never work around them. ## Recursion and fan-out guards - `delegate` spawns parallel child agents; children get a minimal default tool set and ALWAYS have `delegate`, `invoke_workflow`, and `memory` stripped — no fan-out explosions, no child memory writes. - Orchestrator depth is propagated (`depth` → children send `depth+1`) and hard-capped (default max depth 3); a too-deep dispatch is refused. - `max_parallel` caps one delegate call's width by refusal, never by serializing — silent batching would multiply wall clock. ## Pre-built agent cards (`mission_ref`) A stored agent card (markdown frontmatter + body, compiled by the platform) packages a whole mission for reuse: ```yaml - type: harness agent: one-agent goal: "{input}" mission_ref: research-analyst # canonical; `pack:` is an alias # equivalent shorthand: mission: research-analyst (single token = card id) ``` Name `agent: one-agent` on the task (or in the card): a card with no explicit `agent:` still hosts on the legacy `universal-agent` id until the switch-over. Either way the default fence is the thin one above. **Merge order, per key, last wins: card → workflow root → task.** Two rules that bite: - **Arrays REPLACE, never union.** Task-level `skills: [x]` replaces the card's skill list entirely; an explicit `skills: []` deliberately clears it. - A blank inline `mission` text does not override the card's purpose; any non-blank inline text does. Cards are also directly schedulable: `plungeai_schedule {action: "create", mission_ref: "…", schedule: "0 7 * * *", …}` (`plungeai-scheduling`). ## Memory lifecycle At run start the engine recalls the owner's long-term memory as a **frozen snapshot** prepended to the system prompt; the loop's `memory` tool writes back durably (writes do not mutate the running prompt). Bots do **not** set `memory_owner` — policy since 2026-08-23 is to omit it entirely; a bot run reads and writes the owner's general memory namespace, shared across all their bots and runs (a hardcoded per-bot value is rejected by the harness guard unless it names the runner's own identity, so it is redundant at best). Scheduled user runs keep the full memory lifecycle; only system/heartbeat jobs run memory-free. Details, limits, threat-scanning, and the bot-memory policy: `plungeai-memory`. ## Execution semantics worth engraving - **No per-task `retry` on harness.** Mission runs are not idempotent — the engine never re-fires them automatically, and neither should you without checking what the first run already did (`plungeai_executions`). - `success_criteria` are self-checked by the agent (no external verifier in the loop) — write them as verifiable statements, and treat them as guidance-grade, not proof. - Runs pause (never die) on `ask_user` questions and `permissions: ask` gates — status carries a `continuation` block; resume with `plungeai_continue` (`message` for answers, `approve: true` only after an explicit human yes). - Budget honestly: `quick` (4 turns) suits a lookup-and-answer; `deep` (20) a real investigation; explicit `max_turns` for anything unusual. A run that hits its cap ends with whatever it has — better a tight cap and a follow-up mission than an unbounded loop. ## Mission vs workflow — the decision | Signal | Choose | |---|---| | Steps enumerable in advance, repeatable | Workflow of plain tasks (`plungeai-workflows`) | | Open-ended goal, tool choice needs judgment | ONE harness mission | | Both: fixed pipeline with one judgment-heavy stage | Workflow with a single embedded `type: harness` task | The classic failure is decomposing an open-ended goal into six guessed tasks — the guesses are wrong and the pipeline is brittle. Give the goal to one fenced mission instead. --- ## Over the One API — running a mission on REST A mission is one `type: harness` task, so REST runs it on the workflows plane (same `ozk_` key). There is no `plungeai_run_mission` route; the YAML above is the program. ```bash curl -s -X POST https://api.plungeai.com/v1/workflows/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"workflow": {"name": "claim-check", "tasks": [{"type": "harness", "id": "h1", "agent": "one-agent", "goal": "Verify: the EU AI Act applies to medical-device software", "mission": "You are a careful researcher.", "allowed_tools": ["web_search", "web_fetch", "task_complete"], "max_iterations": 8}]}}' # 200 {"success": true, "status": "completed", "workflow_id": "…", "final_task_id": "h1", "request_id": "…", "content": "…"} ``` - **Run paths.** Inline (`POST /v1/workflows/execute`, SSE `…/execute-stream`); a saved mission (`POST /v1/workflows` with `kind: agent`, then `POST /v1/execute {agent: "<id>", input}` or `POST /v1/workflows/{id}/execute {input}`); a pre-built card as an agent (`POST /v1/agents/{id}/execute {prompt}` with the card's id: the `prompt` is the goal, `sync: false` for a `202` pointer). The workflow routes answer when the run is done; none has an async flag, so a long mission uses `execute-stream`, a card with `sync: false`, or a schedule run (`POST /v1/schedules/{id}/run` → `execution_id`). - **Result states.** `status: "completed"`, or `"incomplete"` when the loop stopped at its turn budget before `task_complete` (partial result, the row's outcome is `incomplete`). A pause is `needs_input` / `needs_approval` with a `continuation` on `GET /v1/executions/{id}`, answered by `POST /v1/executions/{id}/continue` (`plungeai-results-traces`). - **Save-time lint** (`kind: agent`, `POST /v1/workflows`): exactly one task, `type: harness`; a declared `allowed_tools` must include `task_complete`; `memory_owner:` is refused. `422 invalid_workflow` carries `errors` and `warnings`; nothing is written. ## MCP tool: `plungeai_run_mission` **Purpose:** run a bounded autonomous agent mission — a loop agent with a goal, a tool fence, and an iteration cap. It recalls and writes the user's long-term memory automatically. **Async by default** (missions are long); pass `mode: "sync"` only for short, tightly-bounded goals. **Parameters** | Param | Type | Notes | |---|---|---| | `goal` | string ≤65536, required | What to achieve. | | `mission` | string | The mission brief / operating instructions (default: a capable general agent that cites sources). | | `allowed_tools` | string[] ≤50 | The tool fence (default `web_search`, `web_fetch`, `task_complete`). The agent cannot use tools outside it. | | `max_iterations` | int 1–50 | Loop cap (default 8). | | `success_criteria` | string[] ≤20 | Explicit done-conditions. | | `persona` | string | Persona id to inject. | | `skills` | string[] ≤20 | Skill ids to inject. | | `agent` | string | The loop runtime the mission runs on (default `one-agent`; a named agent must be active, else an `unavailable` outcome). CLI `missions run --agent`. | | `mode` | `sync` \| `async` | Default async. | **Example** ```json {"user_request": "research the EU AI Act's impact on medical devices", "goal": "Produce a sourced brief on how the EU AI Act affects medical-device software vendors", "max_iterations": 12, "success_criteria": ["cites primary sources", "covers timelines and penalties"]} ``` **Returns (async):** "Started mission `<id>` (async). Poll plungeai_get_workflow_status with this id, then fetch output with plungeai_get_result once completed. The agent recalls and writes long-term memory automatically. If the status reports ⏸ AWAITING USER APPROVAL or AWAITING USER, relay it to the user and continue with plungeai_continue." — follow that script. Sync mode returns the finished result (or a ⏸ pause) directly. **Failures & fixes:** timeouts in sync mode → go async (the remediation says so). A mission's own tool failures are the agent's to work around inside its loop; the envelope classifies anything terminal. Note: async runs emit no progress notifications — poll for status instead of waiting for a stream. # plungeai-models Source: https://www.plungeai.com/docs/skills/plungeai-models Model routing on PlungeAI: how agents/workflows/missions resolve a model through the platform's provider factory (`model`/`provider` fields on a task, mission or `POST /v1/agents/{id}/execute`), and the separate OpenAI-compatible money-plane API (`https://api.plungeai.com/v1/chat/completions`, `/v1/embeddings`, `/v1/models`, plus the OpenRouter drop-in `/api/v1`) with your `ozk_` key, `models[]` fallback routing, `@preset/` bundles and response caching for your own code (`ocean-cli models`). Use when steering which model a task/mission runs on, calling chat/embeddings directly from your app, choosing fallback candidates across providers, reading usage/cost (`/v1/generation`, `/v1/credits`), or debugging a 4xx/5xx from `/v1/chat/completions`. For discovering agents/tools/models by capability use `plungeai-discovery`; for running an agent or tool use `plungeai-agents` / `plungeai-tools-connectors`; for authoring CNL workflows that steer `model` per task use `plungeai-workflows`. [Download zip](https://skills.plungeai.com/plungeai-models.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-models/SKILL.md) PlungeAI touches models in two distinct places, both opened by your one `ozk_` key: 1. **Inside platform runs** — agents, workflows, and missions resolve models through the platform's **provider factory** (a fleet of per-provider Workers behind one selection layer). You steer it with `model`/`provider` fields on tasks and missions. 2. **The money plane** — an OpenAI-compatible inference API at `https://api.plungeai.com` (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`) for YOUR code, with routing/fallback/caching on top. ## Prerequisites - Self-service `ozk_` key from **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`) — the same key opens agents/workflows/MCP and the money plane. - Legacy `sk-ocean-` model keys are retired: none can be minted, existing ones are read-only under Keys → Legacy keys. - Base surfaces: `https://mcp.plungeai.com/v1` (MCP) / `https://api.plungeai.com` (REST). ## Discovery first Model catalogs churn weekly. `GET /v1/models` (with your `ozk_` key) is the priced, live catalog routing candidates are drawn from — discover it at runtime, never hardcode a model list in generated code. ## 1. Steering the model inside a run (provider factory) Flat fields on a task, passed through to the agent: ```yaml - type: task id: analyze agent: one-agent prompt: "…" model: claude-sonnet-5 maxTokens: 4096 ``` Or on a harness mission (mission-level override): ```yaml - type: harness agent: one-agent goal: "…" mission: | You are a careful researcher. model: claude-sonnet-5 provider: anthropic ``` Omit `model` and the agent/provider default applies — usually the right call. Full behavior (pattern-based capability detection so new model generations work with no platform change, auto-continue on truncation, per-task usage/cost) — [`references/provider-factory.md`](/skills/plungeai-models/references/provider-factory). ## 2. The money plane for your own code Drop-in `base_url` swap for any OpenAI-compatible client: ```bash curl -s https://api.plungeai.com/v1/chat/completions \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"model": "anthropic/claude-sonnet-5", "messages": [{"role": "user", "content": "Say hello"}]}' ``` Ordered fallback across providers, reordered by rolling stats before the first attempt: ```json {"models": ["anthropic/claude-sonnet-5", "openai/gpt-5"], "sort": "price", "messages": [{"role": "user", "content": "…"}]} ``` Embeddings and the priced catalog: ```bash curl -s https://api.plungeai.com/v1/embeddings \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"model": "openai/text-embedding-3-small", "input": ["hello", "world"]}' curl -s https://api.plungeai.com/v1/models -H "Authorization: Bearer ozk_YOUR_KEY" ``` Full request fields (`models[]`, `sort`, `@preset/<slug>`, response cache, guardrails), the complete error catalogue, and streaming — [`references/money-plane.md`](/skills/plungeai-models/references/money-plane). ## Over the One API — every route of the plane One `ozk_` key, `Authorization: Bearer` only (no `X-API-Key` here). Setup: `plungeai-api-setup`. | Route | Does | |---|---| | `POST /v1/chat/completions` | OpenAI chat: `model` or `models[]` + `sort`, `@preset/<slug>`, `stream`; the answer's `model` is the slug that served it | | `POST /v1/messages` | the same money path on the Anthropic Messages wire (`x-api-key` or `Authorization: Bearer`, `system`, `tools`, `tool_use`/`tool_result` blocks, `stream` as `message_start … message_stop` events): any catalog model — `anthropic/*` gets the body as sent, every other model a translation; `anthropic-beta` is not forwarded; errors are Anthropic-shaped (`{type: "error", error: {type, message}, request_id}`) | | `POST /v1/completions` | legacy text completion: `prompt` is one string | | `POST /v1/embeddings` | `{model, input}` (string or array); openai, mistral, google and cohere models only (`400 unsupported_provider`) | | `GET /v1/models` | the priced catalog: `{object: "list", data: [{id, owned_by, context_length, max_output, pricing: {in, out}, capabilities, …}]}`, `pricing` in USD per 1M tokens | | `GET /v1/models/{author}/{slug}/endpoints` | the one endpoint that serves a model, with per-token pricing; `404 model_not_found` | | `GET /v1/pricing` | public (no key, cached 60 s): every model Ocean sells — `{object: "list", published_at, data: [{id, mode: "posted" \| "passthrough", list: {in, out}, ocean: {in, out}, discount_pct, offers, …}]}`, USD per 1M tokens; posted models carry Ocean's price and discount against the list price | | `GET /v1/generation?id=` | usage and cost of one completion (`id` = the `x-request-id`, or the `gen-…` id on `/api/v1`); `404 not_found` for another account's id or one not written yet | | `GET /v1/credits`, `GET /v1/key` | `{data: {total_credits, total_usage}}` (USD); the calling key's label, monthly limit, usage and rate limit | | `/api/v1/*` | the same routes in OpenRouter's wire shape for an OpenRouter client: `POST /api/v1/chat/completions`, `/messages`, `/embeddings`, `/completions`, `GET /api/v1/models`, `/generation`, `/credits`, `/key`, `/models/{author}/{slug}/endpoints`; `GET /api/v1/models` is public, errors carry a numeric `code` | MCP has no tool for the money plane: inside a run you steer the model with `model`/`provider` on `plungeai_execute_agent`, a task or a harness mission. The CLI spells the plane `ocean-cli models list | chat | embed`. What builders get wrong: - **Two id spellings.** `GET /v1/models` and the execute routes use `provider/model` (`anthropic/claude-sonnet-5`); `plungeai_list_agents {kind: "models"}` and `GET /v1/discovery/search?kind=models` return registry cards whose ids read `provider:model`. Take the slug from `GET /v1/models`; on the money plane a bare name outside the short alias list is `404 model_not_found`. - **A pinned model that is not in the catalog fails before the run starts** (`422 unknown_model` on `POST /v1/agents/{id}/execute`, with a `hint` to the discovery search) — send `provider` next to `model` so a bare name cannot resolve to another provider. - **Money-plane errors are OpenAI-shaped** (`error.code` is a string such as `invalid_api_key`); execution-plane routes use `{error: {code, message}}`, and `/api/v1` uses OpenRouter's numeric `code`. Branch on the HTTP status. - Cost is opt-in on the response (`usage: {include: true}`); otherwise read the `x-ocean-cost-usd` header or `GET /v1/generation`. ## Gotchas - **One key, Bearer only on the money plane.** The `ozk_` key runs agents/tools/workflows/MCP and the money plane; the money plane takes `Authorization: Bearer` only, not `X-API-Key`. - **Honest billing.** The response `model` field is the slug that ACTUALLY served the request — after a failover it can differ from what you asked for. Bill, log, and display on the response value, never the requested one. - **Never hardcode model slugs.** `GET /v1/models` is the only authority; populate any model picker in generated code from it at runtime. - **Nested `config` is a special case.** A `config: {model, maxTokens}` block is tolerated by `one-agent` only (call mode flattens it) — use flat task fields for every other agent. - **BYOK and guardrails are trust fences.** `402 byok_required`, `403 model_not_allowed`/`content_blocked`, `429 spend_cap_exceeded` — never engineer around them; surface and let the human/org decide. ## Related skills - `plungeai-discovery` — find agents/tools/models/skills live, never from memory. - `plungeai-agents` — execute a single agent (`model`/`provider` overrides on `plungeai_execute_agent` / `POST /v1/agents/{id}/execute`). - `plungeai-tools-connectors` — typed tool execution and contracts. - `plungeai-workflows` — steer `model`/`provider` per task or per harness mission in CNL YAML. - `plungeai-api-setup` / `plungeai-mcp-setup` — connecting your `ozk_` key in the first place. ## Reference pages <CardGroup cols={2}> <Card title="Models plane — /v1/chat/completions, /v1/embeddings, /v1/models" icon="file-text" href="/skills/plungeai-models/references/money-plane"> The OpenAI-compatible surface ("money plane"). </Card> <Card title="Models — provider factory inside runs, money plane for your code" icon="file-text" href="/skills/plungeai-models/references/provider-factory"> PlungeAI touches models in two distinct places. Keep them apart: </Card> </CardGroup> # Models plane — /v1/chat/completions, /v1/embeddings, /v1/models Source: https://www.plungeai.com/docs/skills/plungeai-models/references/money-plane <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/models-proxy.ts, orchestration/api-gateway/docs/guide-3.0/03-models.md, orchestration/api-gateway/docs/guide-3.0/05-openrouter-compat.md, inference/gateway/src/errors.ts, inference/gateway/src/routing/select.ts, inference/gateway/src/metering/response-cache.ts, apps/ocean-dashboard/src/nav.ts, apps/ocean-dashboard/src/components/sections/one-api/KeysTab.tsx, apps/ocean-dashboard/src/components/sections/one-api/CatalogTab.tsx | last-synced: 2026-10-05 (One API 2.3.0: GET /v1/models item shape, embeddings providers, the /api/v1 drop-in; earlier re-verified: error codes against errors.ts, 30s outage exclusion against select.ts, 60-86400s cache TTL clamp against response-cache.ts — all match; fixed stale UI labels — legacy (now retired) sk-ocean- keys and BYOK provider keys live under Dashboard → One API → Keys/Catalog, not "Ocean Dashboard → API keys" or "Settings → Integrations", which do not exist in nav.ts) --> The OpenAI-compatible surface ("money plane"). Auth is the **same `ozk_` key** as the rest of the API, sent as `Authorization: Bearer ozk_YOUR_KEY` only (from Ocean Dashboard → One API → Keys). Errors use the OpenAI error shape `{"error":{"message","type","code","request_id"}}`. ## Drop-in base_url swap Point the OpenAI SDK you already use at the base URL. That is the entire integration: **Python** ```python from openai import OpenAI client = OpenAI(base_url="https://api.plungeai.com/v1", api_key="ozk_YOUR_KEY") r = client.chat.completions.create( model="anthropic/claude-sonnet-5", messages=[{"role": "user", "content": "Hello"}]) print(r.choices[0].message.content) ``` **TypeScript** ```ts import OpenAI from 'openai' const client = new OpenAI({ baseURL: 'https://api.plungeai.com/v1', apiKey: process.env.PLUNGEAI_API_KEY, // ozk_YOUR_KEY }) const r = await client.chat.completions.create({ model: 'anthropic/claude-sonnet-5', messages: [{ role: 'user', content: 'Hello' }], }) ``` **curl** ```bash curl -s https://api.plungeai.com/v1/chat/completions \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"anthropic/claude-sonnet-5","messages":[{"role":"user","content":"Hello"}]}' ``` ```json { "id": "chatcmpl-...", "object": "chat.completion", "model": "anthropic/claude-sonnet-5", "choices": [{ "index": 0, "message": { "role": "assistant", "content": "Hello! How can I help?" }, "finish_reason": "stop" }], "usage": { "prompt_tokens": 8, "completion_tokens": 9, "total_tokens": 17 } } ``` Model slugs are `provider/model`. Get the live priced list from `GET /v1/models` — never hardcode a slug list from memory. The enforcement is **404 `model_not_found`**: an unknown slug fails, so populate model choices from `GET /v1/models` at runtime. Streaming (`"stream": true` → `text/event-stream`), any other OpenAI-compatible field (`tools`, `response_format`, `top_p`, …) passes through untouched. ## POST /v1/chat/completions — request fields beyond OpenAI's | Field | Type | Semantics | |---|---|---| | `model` | string | One slug, or `@preset/<slug>`. Ignored if `models` is set | | `models` | string[] | **Ordered fallback list** — tried in order; first success serves | | `sort` | `"price"` \| `"latency"` \| `"throughput"` | Reorders `models[]` before the first attempt. Providers with no latency/throughput history sort last | | `temperature` | number | Exactly `0` makes the request response-cache eligible | ### Routing & failover (`models[]`) ```bash curl -s https://api.plungeai.com/v1/chat/completions \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"models":["anthropic/claude-sonnet-5","openai/gpt-5"], "sort":"price", "messages":[{"role":"user","content":"Hello"}]}' ``` - Each candidate gets **one retry** (2 attempts total) on `429`/`5xx`/network error; backoff honors a short `Retry-After` (capped 2s) or flat 250ms. - Exhausting a candidate excludes its **provider** for **30 seconds** and moves to the next; a still-excluded candidate shows `"reason":"outage_excluded"` in the logged `route_attempts` (not re-probed). - A non-429 `4xx` (your request is malformed) is **not** retried and **not** failed over — the next candidate would fail identically. - **Honest billing**: the response's `model` field is the slug that **actually served** the request — possibly a fallback, not what you asked for first. Bill, log, and display on the response `model`, never on the requested one. - **Streaming caveat**: failover works identically for `stream:true` — the decision happens on the response status before any bytes are forwarded — but a stream that dies mid-flight is not retried. The `model` field inside SSE chunks reflects the serving upstream natively; billing still uses the honest slug. ## Presets — `@preset/<slug>` A stored model+routing+params bundle, addressed via the `model` field: ```bash curl -s https://api.plungeai.com/v1/chat/completions \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"model":"@preset/fast","messages":[{"role":"user","content":"Hello"}]}' ``` Request-explicit fields **always override** the preset — it only fills gaps. Unknown/inactive slug → **404 `preset_not_found`**. Presets are administered platform-side; ask your admin which exist for your org. ## Response cache (opt-in, per org) - Eligible only when `temperature: 0` is **explicit** and `stream` is not true. - Cache key = hash of the **full request body** — any differing field misses. - Header `x-cache: miss` (stored for next time) / `x-cache: hit` (served from cache, **never billed**, same response id). Header absent entirely when the org has caching off or the request wasn't eligible. - TTL is per-org, clamped 60s–86400s. ## Guardrails & pricing (BYOK-or-billed) Per-org/per-key policy — spend caps, provider/model allow-lists, content-blocking, BYOK requirements — enforced **before** cache lookup and before any money moves. Strictest wins when org and key policies overlap. Pricing: platform-billed by default (provider cost + org markup). Connect your own provider key (BYOK, Dashboard → One API → Catalog → "BYOK provider keys") and requests to that provider route through it for a small routing fee instead. Org policy can *require* BYOK per provider. Trial orgs are metered but never charged. ## Error catalogue | Status | Code | Meaning | Handling | |---|---|---|---| | 400 | `invalid_json` | Request body is not valid JSON | Fix the body | | 401 | `invalid_api_key` | Missing/invalid `ozk_` key | Fix the key | | 402 | `insufficient_balance` | Credit balance at $0 — the first error every platform-billed org hits | Surface: top up (the message carries the URL) | | 402 | `byok_required` | Provider requires your own connected key on this plan | Surface: connect key in Dashboard → One API → Catalog, or change plan | | 403 | `model_not_allowed` | All routing candidates excluded by allow-list | Surface; pick an allowed model (see `GET /v1/models`) | | 403 | `content_blocked` | Prompt matched a guardrail regex (pattern never echoed) | Surface; do not retry variants to probe the filter | | 404 | `model_not_found` | Unknown model slug | Re-pick from `GET /v1/models`; never hardcode slugs | | 404 | `preset_not_found` | `@preset/<slug>` unknown or inactive | Fix the slug | | 429 | `rate_limit_exceeded` | Atomic per-key limiter — **no `Retry-After` on this path** | Fixed short backoff (~1s), then retry | | 429 | `insufficient_quota` | Monthly spend limit reached (type `rate_limit_error`) — distinct from both other 429s | Surface — resets with the billing period; do NOT retry-loop | | 429 | `spend_cap_exceeded` | Guardrail cap reached | Surface — a policy, not a transient; do NOT retry-loop | | varies | `upstream_error` | Provider failure — the provider's own status is passed through (type `api_error`) | One retry with backoff (routing already retried/failed over) | | 500 | `internal_error` | Gateway error | One retry with backoff | | 503 | `money_plane_unavailable` | Inference gateway not bound on this tier | Surface; wrong tier/deployment | Example 402: ```json {"error":{"message":"This provider requires your own API key on your plan — connect it in Settings → Integrations, or upgrade to platform-billed usage","type":"byok_required","code":"byok_required","request_id":"00000000-0000-4000-8000-000000000004"}} ``` ## POST /v1/embeddings Same auth, same guardrail enforcement as chat completions. ```bash curl -s https://api.plungeai.com/v1/embeddings \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"model":"openai/text-embedding-3-small","input":["hello world"]}' ``` ```json { "object": "list", "data": [{ "object": "embedding", "index": 0, "embedding": [0.0123, -0.0456, "..."] }], "model": "openai/text-embedding-3-small", "usage": { "prompt_tokens": 2, "total_tokens": 2 } } ``` `input` is a string or an array of strings; `encoding_format` is `float` or `base64`. The plane serves openai, mistral, google and cohere embedding models — any other provider is `400 unsupported_provider`. ## GET /v1/models — the priced catalog The live list routing candidates are drawn from. Needs the key (`/api/v1/models` is the public one). ```bash curl -s https://api.plungeai.com/v1/models \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "object": "list", "data": [ { "id": "anthropic/claude-sonnet-5", "object": "model", "owned_by": "anthropic", "context_length": 200000, "max_output": 8192, "pricing": { "in": 3.0, "out": 15.0 }, "capabilities": { "streaming": true, "tools": true, "vision": true, "embedding": false, "fim": false }, "pricing_per_token": { "prompt": "0.000003", "completion": "0.000015" } } ] } ``` `pricing.in` / `.out` are USD per **1M** tokens; `pricing_per_token` is the same price per token as decimal strings. `vision: false` means not verified, `max_output: null` means unknown. Generated code that lets users pick a model should populate the choice from this endpoint at runtime, not from a baked-in list. Registry model cards (`plungeai_list_agents {kind: "models"}`, `GET /v1/discovery/search?kind=models`) name the same models as `provider:model`; the plane takes `provider/model`. ## Account, accounting and legacy routes Same `ozk_` key, `Authorization: Bearer` only. | Route | Returns | |---|---| | `GET /v1/models/{author}/{slug}/endpoints` | The endpoint serving one model, with per-token pricing (one upstream per model); `404 model_not_found` | | `GET /v1/generation?id=<gen-… id or x-request-id>` | Post-hoc usage and cost of one completion; `404 not_found` for another account's id | | `GET /v1/credits` | `{data: {total_credits, total_usage}}` — lifetime credits bought and used, USD | | `GET /v1/key` | The calling key: label, monthly limit, usage this month, rate limit | | `POST /v1/completions` | Legacy text completion: `{model, prompt}` (one string; an array is `400 unsupported_parameter`) answered as `text_completion` — prefer `/v1/chat/completions` | ## The `/api/v1` drop-in (OpenRouter wire shape) Point an OpenRouter client at `https://api.plungeai.com/api/v1` with the `ozk_` key: the same plane (routing, guardrails, cache, billing) under the same eight routes — `POST /api/v1/chat/completions`, `/embeddings`, `/completions`; `GET /api/v1/models`, `/generation`, `/credits`, `/key`, `/models/{author}/{slug}/endpoints`. Only the shape differs: - Errors: `{"error":{"code":401,"message":…,"metadata":{"error_type","ocean_code","request_id"}}}` — a numeric `code`, the Ocean code in `metadata.ocean_code`. - The completion `id` is `gen-…` (also the `x-generation-id` header); `GET /v1/generation?id=` takes it. - `GET /api/v1/models` is public and lists OpenRouter's item keys (`pricing` as USD-per-token strings); the `/v1/models` shape above needs a key. - The stream sends `: OPENROUTER PROCESSING` until the first byte and always ends with a `usage` chunk. - OpenRouter-only request fields (`provider`, `route`, `transforms`, `plugins`) are accepted and ignored: route with `models[]`, `sort` and presets. Cost fields (`usage.cost`, `cost_details`, `is_byok`) appear with `usage: {"include": true}`; the non-stream price is also the `x-ocean-cost-usd` header. # Models — provider factory inside runs, money plane for your code Source: https://www.plungeai.com/docs/skills/plungeai-models/references/provider-factory <!-- sources-of-truth: core/core-provider/CLAUDE.md, docs/architecture.md, orchestration/api-gateway/openapi.ts, orchestration/cnl-engine/schema-types.ts | last-synced: 2026-10-05 (GET /v1/models item shape corrected; earlier re-verified: 32 provider dirs + pattern-based detection + 3-turn auto-continue cap against core-provider/CLAUDE.md, InlineMission model/provider/effort + followup provider/model/temperature against schema-types.ts — all match; added the missing caveat that an explicit maxTokens disables auto-continuation entirely, per core-provider/CLAUDE.md line 111) --> PlungeAI touches models in two distinct places. Keep them apart: 1. **Inside platform runs** — agents, workflows, and missions resolve models through the platform's **provider factory**: a fleet of per-provider Workers behind one selection layer. You steer it with `model`/`provider` fields on tasks and missions. 2. **The money plane** — an OpenAI-compatible inference API at `https://api.plungeai.com` (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`) for YOUR code, with routing intelligence on top. Discovery-first applies doubly here: model catalogs churn weekly. `GET /v1/models` is the priced, live catalog — never hardcode a model list in generated code. ## 1. Models inside platform runs (provider factory) Every LLM-capable agent resolves its model through the central provider layer — one Worker per provider (Anthropic, OpenAI, Gemini, Groq, xAI, OpenRouter, Cerebras, Cohere, Mistral, Together, Fireworks, Perplexity, Bedrock, Vertex, and more; the fleet grows — treat any list as illustrative). Each provider Worker: - serves streaming (SSE) and non-streaming completions behind the same interface; - detects model capabilities by **pattern**, not hardcoded lists — new models of a known family work without platform changes (e.g. a new `gpt-5.x` or `o<n>` routes as a reasoning model automatically); - **auto-continues on truncation**: when a completion stops at the output-token limit, the provider re-issues with a continue turn (capped at 3 extra turns) so workflow steps do not silently end mid-sentence — but only when `maxTokens` is left unset. An **explicit** `maxTokens` is a hard cap: it disables auto-continuation and the answer stops at the budget with `finish_reason: length`; - records token usage per task so every run is priced post-hoc — this is where the cost numbers in the `plungeai-results-traces` skill come from. ### Steering model choice in YAML ```yaml # On a plain task — flat fields on the task, passed through to the agent - type: task id: analyze agent: one-agent prompt: "…" model: claude-sonnet-5 maxTokens: 4096 # On a harness mission (mission-level override) - type: harness agent: one-agent goal: "…" mission: | You are a careful researcher. model: claude-sonnet-5 provider: anthropic effort: standard # Follow-up behavior of a saved workflow followup: provider: anthropic # openai | groq | anthropic | gemini model: claude-sonnet-4-5 temperature: 0.7 ``` Flat task fields are the platform spec (`persona`/`model`/`maxTokens` etc. pass through). A nested `config: {model, maxTokens}` block is tolerated by `one-agent` only, which flattens it — do not rely on nesting for other agents. Omit `model` and the agent/provider default applies — usually the right call. Override only when the job demands a specific capability tier (reasoning depth, speed, cost). Provider-specific extras (extended thinking, reasoning effort, search grounding) pass through the same config fields per the agent's card. ## 2. The money plane (OpenAI-compatible, with routing) Auth: **your `ozk_` key** (`Authorization: Bearer` only). Any OpenAI-compatible client works by pointing its base URL at `https://api.plungeai.com/v1`. ```bash curl -s https://api.plungeai.com/v1/chat/completions \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-sonnet-5", "messages": [{"role": "user", "content": "Say hello"}] }' ``` Model slugs are `provider/model` (e.g. `anthropic/claude-sonnet-5`). The response is standard OpenAI shape — with one honest twist: > **`model` in the response is the slug that ACTUALLY served the request.** After a > failover it may differ from what you asked for. Bill and log on the response value. ### Routing intelligence (what you get beyond a raw proxy) ```json { "models": ["anthropic/claude-sonnet-5", "<second choice from /v1/models>", "<third choice from /v1/models>"], "sort": "latency", "messages": [{"role": "user", "content": "…"}] } ``` (Fallback slugs must come from `GET /v1/models` — discover, don't assume.) - **`models[]`** — ordered fallback candidates (alternative to `model`). One retry per candidate on 429/5xx/network, then failover to the next; a provider that just failed is excluded for 30 s. - **`sort`** — `price` | `latency` | `throughput` reorders `models[]` from rolling provider stats before the first attempt (no history sorts last). - **`@preset/<slug>`** as `model` — expands a stored model+routing+params bundle; request-explicit fields still override. - **Response cache** (opt-in per org): exact-match, only for `temperature: 0` non-streaming requests; `x-cache: hit|miss` header when eligible. - **Guardrails** per org/key: spend caps, model allow-lists, content filtering, BYOK gates — enforced server-side on chat AND embeddings. - `stream: true` → `text/event-stream`. ### Embeddings and catalog ```bash curl -s https://api.plungeai.com/v1/embeddings \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"model": "openai/text-embedding-3-small", "input": ["hello", "world"]}' curl -s https://api.plungeai.com/v1/models \ -H "Authorization: Bearer ozk_YOUR_KEY" # → {object: "list", data: [{id, owned_by, context_length, pricing: {in, out}, capabilities, …}, …]} # — the priced catalog routing candidates are drawn from (USD per 1M tokens). Discover, don't assume. ``` ### Money-plane errors — each names its fix | Status | Code | Meaning / action | |---|---|---| | 401 | invalid key | Wrong or missing `ozk_` key | | 402 | `byok_required` | This provider needs the org's own connected key on this plan — connect it in Studio | | 403 | `model_not_allowed` | All routing candidates excluded by an allow-list — pick an allowed model | | 403 | `content_blocked` | Prompt matched a guardrail filter — do not rephrase-to-evade; surface to the user | | 404 | `preset_not_found` | `@preset/<slug>` missing or inactive | | 429 | `rate_limit_exceeded` / `spend_cap_exceeded` | Back off / the org's cap is reached — raising it is a human decision | | 503 | `money_plane_unavailable` | Inference not staged on this tier | Guardrail refusals (402/403/429-cap) are **trust fences**: surface them, never engineer around them. ## Choosing between the two surfaces | Job | Surface | |---|---| | A workflow/mission step needs an LLM | Inside the run: `one-agent` (or a domain agent) with optional `model` override | | Your application needs chat/embeddings directly | Money plane | | You want fallback across providers without writing retry logic | Money plane `models[]` + `sort` | | You need the run traced/priced with the rest of a pipeline | Inside the run — platform observability covers it end-to-end | Both surfaces echo `x-request-id` and accept `x-trace-id` for correlation — see the `plungeai-results-traces` skill. # plungeai-platform Source: https://www.plungeai.com/docs/skills/plungeai-platform THE PlungeAI (Ocean) platform capability map: a growing live registry of agents and structured tools, CNL multi-agent workflows, bounded harness missions, injectable skills/plugins/experts/personas, model routing with an OpenAI-compatible inference API, cron scheduling, per-user long-term memory, and full run observability — how the five layers fit together and which capability skill covers which job. Use when the user asks "what can PlungeAI do", "which PlungeAI capability/agent/tool for X", wants an end-to-end mental model of the platform, hits a trust fence (`403 refused` / `needs_approval` / `409`) and needs the doctrine, or is planning multi-step work and needs to pick between a single agent call, a CNL workflow, and a harness mission. For which door (MCP/REST/CLI/Studio) to use, load `choose-your-plungeai-door` first; each capability's own deep-dive skill (named below) covers its exact tools/params. [Download zip](https://skills.plungeai.com/plungeai-platform.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-platform/SKILL.md) PlungeAI (internally "Ocean") is a Cloudflare-native agent runtime: every agent, tool, and workflow runs as a deployed edge service, connected by RPC. You do not install or host anything — you operate the live platform through one of four doors (MCP, One API, Ocean CLI, Ocean Studio — see `choose-your-plungeai-door`) and compose its capabilities. There is no local mode: everything you execute runs on the deployed platform, and everything you read (catalogs, contracts, results) is live. The platform is **discovery-first by design**: the agent catalog, tool contracts, model list, and workflow inventory change without notice, so look things up ("Discovery first" below) and never assert them from memory. ## Connect MCP (Claude Code shown; other clients use the equivalent `mcpServers` JSON — install page: `https://mcp.plungeai.com/install`; full setup: `plungeai-mcp-setup`): ```bash claude mcp add --transport http plungeai https://mcp.plungeai.com/v1 \ --header "Authorization: Bearer ozk_YOUR_KEY" ``` One API (same `ozk_` key, `Authorization: Bearer` or `X-API-Key`; full setup: `plungeai-api-setup`): ```bash curl -s https://api.plungeai.com/v1/agents?limit=3 \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` Keys are self-service `ozk_` keys from **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`). The same key authenticates the execution planes and the money plane (`/v1/chat/completions`, `/v1/embeddings`, `/v1/models`, Bearer only — `plungeai-models`). Rate limits by tier: free 30/min, pro 100/min (default), enterprise 300/min (the tier table at `GET https://api.plungeai.com/docs` is authoritative; `plungeai_whoami` shows your live window). A plain-text summary for any LLM: `https://mcp.plungeai.com/llms.txt`. ## Verify Before doing real work, prove the connection and identity once: - **MCP:** call `plungeai_whoami` — the identity card: user id, tier, key label, rate-limit window. If it fails, the key or transport is wrong; fix that first. - **One API:** `curl -s https://api.plungeai.com/health` (no auth) proves the router is up; an authenticated `GET /v1/whoami` returns the same identity card (user id, tier, key label) and proves the key. - **CLI:** `ocean-cli whoami`. ## How the platform fits together Five layers, bottom to top — useful when deciding where a problem lives: **Runtime** (agents execute as deployed edge services with one task interface; providers serve models; memory stores state), **Orchestration** (the CNL engine runs workflows as DAGs of agent calls; the scheduler fires them), **Harness** (mission-bounded autonomous runs: tool fence, iteration cap, recursion guard, memory recall), **Capability injection** (skills, plugins, experts, personas, backgrounds, MCP servers injected per run into one loop agent) and **Product** (Ocean Studio to build, dashboards to operate, branded runner apps to use). Two properties explain most behavior you will observe: internal calls are RPC between deployed Workers (so parallel fan-out is essentially free — `plungeai-workflows`), and every result is stored under `(workflow_id, task_id)` (so every execution returns redeemable pointers — `plungeai-memory`). ## Capability map One skill per capability. This table is the router — load the skill before working in that area. The REST column is the One API route family (setup and the three levels: `plungeai-api-setup`; the live contract is `GET /v1/openapi.json`). | Capability | What it gives you | REST | Skill | |---|---|---|---| | **Discovery** | Find agents, tools, models, skills, connectors, templates on the live registry | `/v1/discovery/*`, `/v1/templates` | `plungeai-discovery` | | **Agents** | Registry building-block agents: discover and execute one; run your saved agent or bot by id | `/v1/agents`, `POST /v1/execute` | `plungeai-agents` | | **Tools & connectors** | Structured tool-agents with typed contracts (params, operations, approval gates, connected accounts) | `/v1/tools`, `/v1/connections`, `/v1/channels` | `plungeai-tools-connectors` | | **Workflows** | CNL YAML multi-agent pipelines; saved workflows, agents and bots | `/v1/workflows` | `plungeai-workflows` | | **Bots** | Saved agents with stored policy (permission classes, locks, spend caps), workspace, paired chats, groups | `/v1/workflows/{id}/permissions`, `/v1/bot-groups` | `plungeai-bot-agent` | | **Models** | Provider factory inside runs + OpenAI-compatible inference API | `/v1/chat/completions`, `/v1/embeddings`, `/v1/models` | `plungeai-models` | | **Skills / Plugins / Experts / Personas** | Injectable instruction packs, plugins, voice/domain expertise, standing context | `/v1/skills`, `/v1/discovery/search?kind=skills` | `plungeai-skills-plugins` | | **Missions** | Bounded autonomous agent runs: goal, tool fence, iteration cap, success criteria | a `type: harness` task on `/v1/workflows/execute` | `plungeai-missions` | | **Scheduling** | Cron, one-time and webhook/email/hook-triggered runs of workflows, agents and missions | `/v1/schedules`, `/v1/triggers` | `plungeai-scheduling` | | **Campaigns** | Ledgered, claim-based batch work across many rows | `/v1/workflows` with `kind: campaign` | `plungeai-campaigns` | | **Memory** | Run-data exchange (SharedMemory) + per-user long-term memory | `/v1/memory` | `plungeai-memory` | | **Results & traces** | Live SSE events, execution history, steer a run, traces, cost | `/v1/executions`, `/v1/traces/{id}`, `/v1/mcp/runs` | `plungeai-results-traces` | | **Chat** | Persistent platform chat with sessions (`plungeai_chat`) | MCP and CLI only | `plungeai-mcp-setup` | ## Which capability for which job - **One-shot capability call** ("search the web", "convert this PDF"): a single agent — prompt-driven → `plungeai_execute_agent`; structured (has a parameters table) → `plungeai_get_tool_contract`, then `plungeai_execute_tool` (`plungeai-agents`, `plungeai-tools-connectors`). - **Multi-step pipeline with known steps** ("research A, B, C in parallel, then synthesize"): a CNL workflow (`plungeai-workflows`). - **Open-ended goal needing judgment** ("investigate X and produce a memo"): ONE harness mission, not many small guessed tasks (`plungeai-missions`). - **Behavior or knowledge a run should carry** (style guide, method, company context): skills, plugins, experts, personas, backgrounds (`plungeai-skills-plugins`). - **Raw LLM inference in your own code**: the One API money plane (`plungeai-models`). - **"Every morning" anything**: schedule it (`plungeai-scheduling`). **Batch work over many rows**: a campaign (`plungeai-campaigns`). **"Remember this"**: long-term memory (`plungeai-memory`). **"Why did that run fail / what did it cost"**: `plungeai-results-traces`. ## Discovery first (non-negotiable) Catalogs are live and execution refuses stale ids. Always: 1. **Agents:** `plungeai_list_agents {search: "<capability in plain words>"}` — hybrid semantic + keyword search; trust the ranking. REST: `GET /v1/agents`, `GET /v1/discovery/search?q=…`. Take ids verbatim (kebab-case, e.g. `brave-agent`) from results only. 2. **Injectable ids** (skills, experts, personas, models, workflows, connectors): the same tool with a `kind` filter (REST: `GET /v1/discovery/search?kind=…`). A typo'd id silently degrades to a warning (`plungeai-skills-plugins`). 3. **Tool shapes:** `plungeai_get_tool_contract {agent_id}` or `GET /v1/tools/{id}` before the first call to an unfamiliar structured agent — the contract IS the docs. 4. **Routes and models:** `GET /v1/openapi.json` and `GET /v1/models` (same `ozk_` key, Bearer) — never invent One API paths or model names. 5. **The user's own inventory:** `plungeai_list_workflows` / `GET /v1/workflows` — in user parlance "my agents" means their saved workflows, not the registry. Full mechanics (search params, cards, contracts, templates) — `plungeai-discovery`. ## Trust fences and structured outcomes The platform answers with structured outcomes, never raw errors. Respect them: - **Over MCP the outcome is an envelope, not an HTTP error**: every tool answers HTTP 200 with a status of `ok`, `needs_input`, `needs_connection`, `needs_api_key`, `needs_approval`, `unavailable`, or `error` (there is no `refused` MCP status — fences spell as HTTP codes only on the One API). - **`403 refused`** (One API): a fence blocked the call — a gated or money operation attempted unattended. Surface it. **Never retry**; a fence is a policy, not a flake. - **`409 approval_required`** / **`needs_approval` + a paused run (`⏸ AWAITING USER APPROVAL`)**: a human must decide. Relay the approval block verbatim, wait for the explicit answer, then `plungeai_continue` (`approve: true` ONLY after they said yes; a refusal goes in `message`) — over REST `POST /v1/executions/{id}/continue` with `{approve}` or `{answer}`. Never approve on your own. - **`needs_input`** (`422 invalid_params`): the response names the missing fields and carries the schema — fix exactly those, retry once. - **`needs_connection` / `needs_api_key`** (REST `424 connection_required`, with a `connect` pointer): the acting user must connect a credential — in Studio, or over the API with `POST /v1/connections/{provider}`. Tell them exactly what to connect; retry after. - **`unavailable`**: the outcome lists live alternatives — pick one or re-discover. Same doctrine everywhere: follow the remediation; never blind-retry the identical call. ## A complete first session (MCP) The canonical shape of operating the platform, end to end — adapt the middle to the job: ``` 1. plungeai_whoami → identity card (proves key, tier, rate window) 2. plungeai_list_agents {search: "web search", user_request: "…"} → ranked cards; take ids verbatim (e.g. brave-agent, exa-agent) 3. plungeai_execute_workflow {user_request: "…", input: "solid-state battery commercialization", workflow_yaml: "<a parallel brave-agent + exa-agent search feeding a one-agent brief>"} → result (final markdown — relay verbatim) For >3 min jobs: mode: "async" → plungeai_get_workflow_status → plungeai_get_result 4. Output is right → plungeai_workflow {action: "create", name, yaml, description} 5. plungeai_schedule {action: "create", job_type: "workflow", target: "<saved id>", schedule: "0 7 * * *"} → runs every morning; verify once with {action: "run_now"} ``` Steps 2-3 change per job (single agent call, tool contract + typed execution, or a harness mission via `plungeai_run_mission`) — the frame (verify → discover → test → save → schedule) does not. The same frame over the One API: `GET /v1/whoami` → `GET /v1/discovery/search` → `POST /v1/workflows/execute` → `POST /v1/workflows` → `POST /v1/schedules`. ## Operating rules - **Every `plungeai_*` call also takes `user_request`** — the user's ask verbatim (the platform uses it for routing and support diagnostics). - **Async for long runs.** Over ~3 minutes: `mode: "async"` (on `plungeai_execute_workflow` / `plungeai_execute_tool` / `plungeai_run_mission`; `plungeai_execute_agent` has none — wrap it in a one-task workflow) → poll `plungeai_get_workflow_status` → `plungeai_get_result`. Over REST: `GET /v1/executions/{id}`. - **Results are final, user-ready markdown.** Present them verbatim and in full — never re-format or re-type them. Over MCP `format: "json"` returns the same result as a JSON document; REST `format` is `json`, `yaml`, `markdown` or `text`. - **Test before saving.** Run ad-hoc (`plungeai_execute_workflow {workflow_yaml}`) and read the output before `plungeai_workflow {action: "create"}`. - **Idempotency:** payment-, messaging- and automation-class agents may duplicate side effects on re-runs; check execution status before re-firing. ## Common pitfalls | Pitfall | Reality | |---|---| | "Show me my agents" answered with the registry | Users mean their saved workflows → `plungeai_list_workflows` | | Baking dates into scheduled workflows | Date tokens (`{week_start}`, …) roll automatically (`plungeai-scheduling`) | | Storing context as long-term memory that belongs in a background/skill | Memory is learned per-user state; authored shared context goes in backgrounds/skills (`plungeai-memory`) | | Inventing One API routes or model names in generated code | `GET /v1/openapi.json` and `GET /v1/models` are the only authorities | ## Related skills - `choose-your-plungeai-door` — which door (MCP/REST/CLI/Studio) for which job. - `plungeai-mcp-setup` / `plungeai-api-setup` / `plungeai-cli-setup` — connect and verify a client. - `plungeai-sdk-setup` — the official One API SDKs (TypeScript, Python, Go, Rust). - Every capability in the map above has its own deep-dive skill by the same name. - `plungeai-in-<tool>` — wiring a specific coding tool's agent to PlungeAI over MCP. # plungeai-results-traces Source: https://www.plungeai.com/docs/skills/plungeai-results-traces Debug and observe PlungeAI runs: live SSE events, plungeai_get_workflow_status / plungeai_executions history, persisted GET /v1/traces/{id} spans and gateway request cost, the HITL conversation loop (plungeai_continue/plungeai_followup/plungeai_chat), and outbound MCP door runs (POST /v1/mcp/runs), and the REST/MCP/CLI execution verbs (`/v1/executions`: output, continue, followup, steer a running run, files a run produced). Use when a run seems hung, failed, or expensive, when the user wants to correlate calls with x-trace-id, or when resuming a paused ⏸ run. For a run's actual output content use `plungeai-memory`'s SharedMemory section; for authoring the workflow being observed use `plungeai-workflows`. [Download zip](https://skills.plungeai.com/plungeai-results-traces.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-results-traces/SKILL.md) Every execution is observable at four altitudes: live events while it runs, a status/history record after, a persisted span-level trace, and the gateway's own request log with cost. Read them in this order instead of re-running the workload to "see what happens." ## Prerequisites - A self-service `ozk_` API key from **Dashboard → One API → Keys** (https://dashboard.plungeai.com), or an MCP client connected to `https://mcp.plungeai.com/v1`. - Always capture the ids a run gives you: `workflow_id`/`execution_id`, `request_id`, and your own `x-trace-id`. Without them you are grepping timestamps. ## Correlation ids — wire them in from the start Every One API response echoes a server-minted `x-request-id`; send your own `x-trace-id` header to correlate a whole multi-call operation. **It must be a UUID**: any other value is ignored, and the trace id is then that call's `x-request-id` (still traceable). A replayed UUID on an execute route is `409 duplicate_execution_id`. ```bash TRACE=$(uuidgen | tr 'A-Z' 'a-z') curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -H "x-trace-id: $TRACE" -d '{"prompt": "Say: traced"}' curl -s https://api.plungeai.com/v1/traces/$TRACE -H "Authorization: Bearer ozk_YOUR_KEY" ``` `GET /v1/traces/{id}` answers `{trace_id, spans, gateway_requests}`. An id that is unknown, or belongs to another key, is `404 not_found` (the same answer for both). Spans and request logs land a few seconds after the run, so retry a `404` for a short capped while right after a call, then stop. (`ocean-cli traces get ID` is the CLI spelling.) ## The debugging playbook 1. **Run seems hung** → `plungeai_get_workflow_status` (self-heals stuck rows; may return a `continuation` block — the run is waiting on a human, not hung). 2. **Run failed** → status `error_message` → the trace's failing span (`status`, `agent`, `duration_ms`). 3. **Output is thin** → execution summary `failed_branches[]` and `tasks_skipped`. 4. **Slow** → per-task durations from SSE events/spans; a staircase against one destination means queueing there, not in the engine. 5. **Expensive** → trace `gateway_requests[].cost_usd` by route. 6. **Instructions seemingly ignored** → the run's own warnings (capability ids that degraded — `plungeai-skills-plugins`). Full playbook, execution-summary fields, and quality signals: [`references/observability.md`](/skills/plungeai-results-traces/references/observability). Full `GET /v1/traces/{id}` shape and error rules: [`references/traces.md`](/skills/plungeai-results-traces/references/traces). ## The ⏸ human-in-the-loop loop A run pauses (never dies) on an `ask_user` question or an approval gate, surfaced as `continuation` on `plungeai_get_workflow_status`. Relay the block verbatim, wait for the human's words, then: - `plungeai_continue {execution_id, approve: true}` — ONLY after an explicit yes. - `plungeai_continue {execution_id, message: "<their words>"}` — anything else. Never approve on your own, never rephrase the pending action, never retry around a pause. Once a run is COMPLETE, use `plungeai_followup {execution_id, prompt}` instead — `continue` on a finished run redirects you there. Free-standing chat with no run in sight is `plungeai_chat`. Full parameter tables and failure messages: [`references/conversation.md`](/skills/plungeai-results-traces/references/conversation). ## Executions over REST, MCP and CLI — steer a live run, collect its files Every run is an execution, whichever door started it (REST, CLI, MCP, Studio, a schedule, a trigger, a chat). One resource, three spellings: | Job | REST (`ozk_` key) | MCP `plungeai_executions` / tool | CLI `ocean-cli executions …` | |---|---|---|---| | list | `GET /v1/executions?source=&status=&workflow_id=&limit=&offset=` (`status`: `running`, `completed`, `failed`, `cancelled`; limit ≤ 100, default 20) | `list` | `list` | | poll | `GET /v1/executions/{id}` — `status`, or `needs_input` / `needs_approval` with a `continuation` | `get`, `plungeai_get_workflow_status` | `get`, `status`, `poll` | | output | `GET …/{id}/output?format=` (`json`, `yaml`, `markdown`, `text`) | `output` | `output` | | answer or approve a pause | `POST …/{id}/continue {answer \| approve, allow_scope?}` (`once`, `run`, `always`) | `plungeai_continue {message \| approve}` | `continue ID [--message TEXT \| --approve]` | | talk to a run | `POST …/{id}/followup {text}` (202 queued while running, 200 new turn once done); `GET …/{id}/conversation` | `plungeai_followup {prompt}`; `conversation` | `followup ID PROMPT`; `conversation` | | **steer a RUNNING run** | `POST …/{id}/steer {text}` — queued for its next turn (202); `409 execution_finished` if over | `steer {execution_id, message}` | `steer ID TEXT` | | **files the run produced** | `GET …/{id}/files` — spreadsheets, documents, decks | `files` | `files ID` | | cancel, re-attach, delete | `POST …/{id}/cancel`, `GET …/{id}/stream` (SSE: `status`, `progress`, `done`; `?task=` up to 4), `DELETE …/{id}` | `cancel`, `delete` | `cancel`, `delete` | `steer` is for a run still in flight (a bot mid-loop that should change course); `followup` on a finished run starts a new turn in its conversation. A run that reaches its stored spend cap says so in its result (`[run stopped at $… per-run budget cap …]`). Ids are the `workflow_id` / `execution_id` of the call that started the run (or its engine id `exec-…`). What builders get wrong: - **The message field is spelled three ways**: REST `text` (`followup`, `steer`) and `answer` (`continue`), MCP `prompt` (`plungeai_followup`) and `message` (`plungeai_continue`, `steer`), the CLI a positional argument. - **Another account's id answers `404 execution_not_found`**, like a missing one. A run made for your customer (`POST /v1/execute` with `on_behalf_of`) is read with `?on_behalf_of=<customer id>` on `get`, `output` and `files`. - **`output` has its own states**: `404 not_ready` (still running: poll), `409 execution_failed` (the message is the run's error), `410 result_expired`. `continue` on a finished run is `409 not_paused` (use `followup`); `410 session_expired` means no conversation session is left. - A run takes at most 5 live `stream` attachments (`429 too_many_streams`); a finished run is `409 execution_finished`. - `GET /v1/workflows/results/{workflowId}/{taskId}` and `GET /v1/agents/results/{workflowId}/{taskId}` read one task's stored output (`plungeai-memory` has SharedMemory); `POST /v1/workflows/executions/{id}/cancel` is the same cancel as `POST /v1/executions/{id}/cancel`. Route detail: `plungeai-api-setup` → `references/bots-and-door.md`. ## Outbound MCP door runs Separately from the platform's own inbound MCP tools, you can open a metered run against third-party MCP servers: `POST /v1/mcp/runs {server_ids: [...]}` → call namespaced tools (`mcp__<server>__<tool>`) → `DELETE` to close. Runs expire after 30 idle minutes, and a closed, expired or unknown run id is `404 run_not_found` (retrying never revives it: open a new run). Full lifecycle, error shapes, and a working TypeScript pattern: [`references/mcp-runs.md`](/skills/plungeai-results-traces/references/mcp-runs). CLI: `ocean-cli mcp runs create | tools | call | delete`. ## Gotchas - Per-task duration is measured from SERVER timestamps in the SSE stream — never time a run from your own clock around the request. - `payload_inline` on a trace span is guard-scanned and capped at 1 KB; larger payloads sit behind `payload_ref` (not fetchable through this API). - A scheduler run id is not directly resolvable by `plungeai_get_workflow_status` / `plungeai_get_result` — go through `plungeai_schedule {action: "runs"}` first (`plungeai-scheduling`). - Full `plungeai_get_workflow_status` / `plungeai_executions` parameter and return contracts live in `plungeai-workflows` (`references/mcp.md`) — this skill covers what to do with them once you have a run's ids. ## Related skills - `plungeai-workflows` — full `plungeai_get_workflow_status` / `plungeai_executions` contracts, and authoring the workflow being observed. - `plungeai-missions` — mission-specific pause/resume and iteration-cap semantics. - `plungeai-scheduling` — a scheduled job's own run ledger. - `plungeai-memory` — SharedMemory (`plungeai_get_result`), a run's actual output. - `plungeai-skills-plugins` — the capability-resolution warnings this playbook checks. ## Reference - [`references/observability.md`](/skills/plungeai-results-traces/references/observability) — correlation ids, SSE event order, execution summary, the debugging playbook, quality signals. - [`references/traces.md`](/skills/plungeai-results-traces/references/traces) — full `GET /v1/traces/{id}` route detail. - [`references/conversation.md`](/skills/plungeai-results-traces/references/conversation) — `plungeai_continue` / `plungeai_followup` / `plungeai_chat` and the ⏸ HITL protocol. - [`references/mcp-runs.md`](/skills/plungeai-results-traces/references/mcp-runs) — outbound MCP door: open/list/call/close a run. ## Reference pages <CardGroup cols={2}> <Card title="Conversation — chat, followup, continue (and the ⏸ HITL loop)" icon="file-text" href="/skills/plungeai-results-traces/references/conversation"> Three distinct conversation surfaces — pick by what the user is doing: </Card> <Card title="MCP plane — /v1/mcp" icon="file-text" href="/skills/plungeai-results-traces/references/mcp-runs"> Two directions in one plane: </Card> <Card title="Observability — watching, debugging, and pricing platform runs" icon="file-text" href="/skills/plungeai-results-traces/references/observability"> Every execution on PlungeAI is observable at four altitudes: live events while it runs, an execution record after it finishes, a persisted span-level trace… </Card> <Card title="Traces — GET /v1/traces/{id}" icon="file-text" href="/skills/plungeai-results-traces/references/traces"> The observability plane: one id follows a request across every internal hop. </Card> </CardGroup> # Conversation — chat, followup, continue (and the ⏸ HITL loop) Source: https://www.plungeai.com/docs/skills/plungeai-results-traces/references/conversation <!-- sources-of-truth: orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/conversation.ts, orchestration/mcp-gateway/chat.ts, orchestration/mcp-gateway/server.ts, orchestration/api-gateway/openapi-executions.ts | last-synced: 2026-10-05 --> Three distinct conversation surfaces — pick by what the user is doing: | Surface | Anchored to | Use when | |---|---|---| | `plungeai_chat` | A standing conversation (its own `conversation_id`) | Open-ended chat with the platform assistant, across days. | | `plungeai_followup` | A COMPLETED execution (`execution_id`) | "Ask more about that run" — the run's result is the context. | | `plungeai_continue` | A PAUSED execution (`execution_id`) | The run stopped with a ⏸ block — a question or a pending approval. | The execution id doubles as the conversation session id: every follow-up and continue turn on a run lands in that run's conversation thread, which `plungeai_get_result` and `plungeai_executions {action: "conversation"}` render in full (Studio shows the same thread). --- ## The ⏸ HITL loop (read this first) A conversational or gated agent stops in one of two states, appended to the run's result and surfaced by `plungeai_get_workflow_status` as `structuredContent.continuation`: ``` ⏸ AWAITING USER APPROVAL: <action summary> (<price>) Ask the user to confirm, then call plungeai_continue with execution_id "<id>" and approve: true (or message: "<their words>"). ``` ``` ⏸ AWAITING USER: <question> Relay this to the user, then call plungeai_continue with execution_id "<id>" and message: "<their answer>". ``` In the status tool's machine mirror these map to `continuation.status: "needs_approval"` and `"question"` respectively. **The protocol, exactly:** 1. Relay the block to the user VERBATIM (it is theirs, not yours) and ask them to decide ("Approve this action? yes/no"). 2. Wait for their words. 3. `plungeai_continue {execution_id, approve: true}` ONLY for an explicit yes. Anything else — an answer, a denial, a change of course — goes as `message: "<their words>"`. 4. The reply either ends with another ⏸ block (loop again) or with `✅ Conversation resolved (no further input needed)`. Approval fences are trust fences: never approve on your own, never rephrase the pending action, never "retry" around a pause. Async runs surface pending approvals via `plungeai_get_workflow_status` and `plungeai_get_result` — the same protocol applies from there. --- ## plungeai_continue **Purpose:** resume a paused conversation — answer the agent's question or deliver the user's approval decision. **Parameters** | Param | Type | Notes | |---|---|---| | `execution_id` | string, required | The id from the ⏸ block. | | `message` | string | The user's words: an answer, a "no", a modification. | | `approve` | boolean | `true` only after the user explicitly confirmed. | One of `message` / `approve: true` is required. **Example (approval):** `{"user_request": "yes, book it", "execution_id": "d3ad...", "approve": true}` **Example (answer):** `{"user_request": "the London office, not Paris", "execution_id": "d3ad...", "message": "The London office, not Paris"}` **Returns:** the agent's next turn (an approved action executes during this call — it can legitimately take ~30s), then either a fresh ⏸ block or `✅ Conversation resolved`. Both turns are saved to the run's thread. **Failures & fixes** - "Provide `message` or `approve: true`" → you sent neither. - "Execution not found" → wrong/foreign id; take it from the ⏸ block itself. - "No conversation session for this execution (expired or not a conversational agent)" → the pause window lapsed or the run never paused; use `plungeai_followup` for a finished run. - "This run isn't paused — it already completed. Use plungeai_followup..." → exactly that; `continue` is only for pending sessions. (A late `approve: true` on an already-consumed approval is safe — it answers that nothing is awaiting approval rather than re-running the action.) - "Agent continuation failed" → the engine failed OR the reply just didn't land within the ~30s poll while the action may still have executed. Retrying `approve: true` once is safe (the approval token is idempotent — the action never runs twice). For a `message`, check `plungeai_get_result` first — if the turn landed, a retry would send the message again as a second turn. Then surface to the user. --- ## plungeai_followup **Purpose:** ask a follow-up about a COMPLETED execution. Session-first: if the run's agent left a session, the SAME agent continues with its own context. Otherwise a research fallback answers using the run's stored result as prior context (and is instructed to web-verify any new factual claim — never to invent). **Parameters:** `execution_id` (required), `prompt` (required — the user's follow-up, ≤65536). **Example:** `{"user_request": "what about their European competitors?", "execution_id": "d3ad...", "prompt": "What about their European competitors?"}` **Returns:** the answer (relay verbatim), possibly ending in a ⏸ block if the continued agent paused again. The turn is appended to the run's conversation thread — a later `plungeai_get_result` on the same id shows the whole dialogue. **Failures & fixes:** "Execution not found" → wrong/foreign id. "Failed to retrieve follow-up response" → transient; retry once. If the stored result has expired the fallback researches from scratch — the answer may lack the original run's specifics; say so if it matters. --- ## plungeai_chat **Purpose:** persistent chat with the PlungeAI assistant — a conversational agent with web search and registry lookup. Conversations persist and appear identically in Studio's Think tab and the CLI. **Actions** | Action | Requires | Notes | |---|---|---| | `send` | `message` | Streams a reply. Omit `conversation_id` to auto-create a conversation (titled from the first message); pass it to continue one. The reply ends with `(conversation: <id>)` — reuse that id for every subsequent turn. | | `new` | — | Explicitly create a conversation first (optional `message` seeds the title); returns the id. | | `list_sessions` | — | Table of the user's conversations (Title, ID, Messages). | | `history` | `conversation_id` | The full transcript, rendered. | **Example:** `{"user_request": "what can this platform do?", "action": "send", "message": "What can this platform do?"}` **Returns:** the assistant's markdown + the conversation-id footer. Relay both — the footer is how the user (and you) come back to the thread. **Failures & fixes:** "message is required for send." / "conversation_id is required for history." → supply it. "Conversation not found" → wrong or foreign id; `list_sessions` to find the real one. Avoid firing two `send` calls into the same conversation concurrently — turns are persisted one-writer-at-a-time and a racing turn can be lost. --- ## The same loop over REST (`/v1/executions`, One API 2.3.0) `plungeai_continue` / `plungeai_followup` have REST equivalents, so a pure-HTTP client can run the ⏸ loop too. `GET /v1/executions/{id}` reads `status` (`needs_input` / `needs_approval`) and `continuation`. `POST /v1/executions/{id}/continue` takes `{"answer": "…"}` or `{"approve": true, "allow_scope": "once" | "run" | "always"}` (`409 not_paused` once the run completed: use `POST …/followup {"text": "…"}`; `410 session_expired` when no conversation session is left). `POST …/steer {"text": "…"}` queues a message for a run still in flight (`202`); `GET …/files` lists what the run produced. Approve only after the human's explicit yes, exactly as above. ## Choosing among the three (quick rules) - ⏸ block on screen → `plungeai_continue`. Nothing else resumes a pause. - "Ask more about that run/report" → `plungeai_followup` with that run's id. - Free-standing conversation, no run in sight → `plungeai_chat`. - `continue` on a finished run redirects you to `followup`; `followup` on a paused run continues the pending session (it is session-first) — but the approval decision itself must still travel via `continue`. # MCP plane — /v1/mcp Source: https://www.plungeai.com/docs/skills/plungeai-results-traces/references/mcp-runs <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/mcp.ts, orchestration/mcp-executor/mcp-executor.ts | last-synced: 2026-10-05 (One API 2.3.0: an unknown or expired run is 404 run_not_found) --> Two directions in one plane: - **Inbound** — the platform *is* an MCP server: point any MCP client at it and get the whole platform (execute agents, workflows, discovery, …) as MCP tools. - **Outbound** — the platform is an MCP *client* on your behalf: open a "run" against third-party MCP servers from the catalog and call their tools through one metered, tenant-isolated door. Auth: `Authorization: Bearer ozk_YOUR_KEY` on every route here. ## Inbound — use the platform from any MCP client Canonical Streamable-HTTP endpoints (both accept an `ozk_` bearer): - `https://mcp.plungeai.com/v1` — the dedicated MCP host - `https://api.plungeai.com/v1/mcp` — the same surface through the One API router Claude Code: ```bash claude mcp add plungeai --transport http https://mcp.plungeai.com/v1 \ --header "Authorization: Bearer ozk_YOUR_KEY" ``` Generic `mcpServers` JSON (Cursor, clients with config files): ```json { "mcpServers": { "plungeai": { "type": "http", "url": "https://mcp.plungeai.com/v1", "headers": { "Authorization": "Bearer ozk_YOUR_KEY" } } } } ``` Per-client install steps (Claude Desktop, Cursor variants, VS Code, …) are the per-editor `plungeai-in-*` skills' job (e.g. `plungeai-in-cursor`); this file covers the HTTP surface. Raw JSON-RPC 2.0 works too — `POST /v1/mcp` with the message as the body; responses come back as JSON, or SSE for streamed tool calls. The `Mcp-Session-Id` header is forwarded both ways — session-based flows must replay the one the server returns. ### GET /v1/mcp/tools — what the platform exposes as MCP tools Returns the standard JSON-RPC `tools/list` result (tool names like `plungeai_execute_workflow`, `plungeai_execute_agent`, …). List it live — the tool set evolves; never hardcode names or counts. ```bash curl -s https://api.plungeai.com/v1/mcp/tools \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "plungeai_execute_agent", "description": "Execute a platform agent with a prompt", "inputSchema": { "type": "object", "properties": { "...": "..." } } }, { "name": "plungeai_execute_workflow", "description": "...", "inputSchema": { "...": "..." } } ] } } ``` ## Outbound — MCP runs Lifecycle: **open a run → list/call its tools → close it**. Runs are tenant-isolated (scoped to your key's identity) and **expire after 30 idle minutes**; close them explicitly when done anyway. Find `server_ids` in the catalog first: `GET /v1/discovery/search?kind=mcp-servers&q=<what you need>` (`plungeai-discovery`) — ids come from there, never from memory. ### POST /v1/mcp/runs — open a run Body: `{"server_ids": ["<registry mcp-server card id>", ...]}`. ```bash curl -s -X POST https://api.plungeai.com/v1/mcp/runs \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"server_ids": ["cloudflare-docs"]}' ``` HTTP **201**: ```json { "run_id": "00000000-0000-4000-8000-000000000007", "tools": [ { "name": "mcp__cloudflare-docs__search_cloudflare_documentation", "description": "Search Cloudflare documentation", "input_schema": { "type": "object", "properties": { "query": { "type": "string" } } } } ], "connected": ["cloudflare-docs"], "failed": [], "warnings": [], "skipped": [] } ``` Tool names are namespaced `mcp__<server>__<tool>`. Partial success is real: check `connected` vs `failed` vs `skipped` — a run can open with some servers up and others down (per-server errors are in `failed`). Errors: `400 invalid_body` (missing/empty `server_ids`), `502 no_servers_connected` — **none** of the requested servers came up; the body carries per-server failures plus the same `skipped` array. A server that just failed sits in a **60s cooldown**: re-opening immediately lists it in `skipped` (not re-attempted) while other servers connect normally. Wait out the cooldown before retrying it. ### GET /v1/mcp/runs/{id}/tools — the run's tool table ```bash curl -s https://api.plungeai.com/v1/mcp/runs/00000000-0000-4000-8000-000000000005/tools \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "tools": [ { "name": "mcp__cloudflare-docs__search_cloudflare_documentation", "description": "...", "input_schema": { "...": "..." } } ], "count": 1 } ``` An unknown, closed or expired run id is `404 run_not_found` (the run is never revived by retrying): open a new run. An empty `tools` array with `200` is a live run whose servers list no tools. ### POST /v1/mcp/runs/{id}/call — call one tool Body: `{"name": "<namespaced tool>", "args"?: object, "timeout_ms"?: integer}`. ```bash curl -s -X POST https://api.plungeai.com/v1/mcp/runs/00000000-0000-4000-8000-000000000005/call \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "mcp__cloudflare-docs__search_cloudflare_documentation", "args": {"query": "durable objects alarms"}}' ``` ```json { "content": "# Search results\n\n1. Durable Objects Alarms — ..." } ``` Errors: `400 invalid_body` (no `name`), `502 tool_call_failed` — the message is always the fixed string `"MCP tool call failed"`; the upstream detail lives in the error envelope's **`content`** extra. One retry with backoff is reasonable. A run that is unknown, closed or idle-expired is `404 run_not_found` — **open a new run**, don't debug the tool name. ### DELETE /v1/mcp/runs/{id} — close a run Idempotent — deleting an already-gone run still returns success. ```bash curl -s -X DELETE https://api.plungeai.com/v1/mcp/runs/00000000-0000-4000-8000-000000000005 \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "success": true } ``` ## Full outbound pattern (TypeScript, plain fetch) ```ts const BASE = 'https://api.plungeai.com' const H = { Authorization: `Bearer ${process.env.PLUNGEAI_API_KEY}`, // ozk_YOUR_KEY 'Content-Type': 'application/json' } // 1. open const run = await (await fetch(`${BASE}/v1/mcp/runs`, { method: 'POST', headers: H, body: JSON.stringify({ server_ids: ['cloudflare-docs'] }), })).json() if (!run.connected?.length) throw new Error(JSON.stringify(run.failed)) // 2. call (tool name taken from run.tools — never hardcoded) const tool = run.tools[0].name const res = await (await fetch(`${BASE}/v1/mcp/runs/${run.run_id}/call`, { method: 'POST', headers: H, body: JSON.stringify({ name: tool, args: { query: 'workers kv limits' } }), })).json() console.log(res.content) // 3. close await fetch(`${BASE}/v1/mcp/runs/${run.run_id}`, { method: 'DELETE', headers: H }) ``` # Observability — watching, debugging, and pricing platform runs Source: https://www.plungeai.com/docs/skills/plungeai-results-traces/references/observability <!-- sources-of-truth: orchestration/api-gateway/openapi.ts (traces, request log), orchestration/cnl-engine/README.md (SSE events, execution summary), orchestration/cnl-engine/executors.ts, orchestration/mcp-gateway/server.ts, orchestration/scheduler/README.md, orchestration/api-gateway/api-gateway.ts | last-synced: 2026-10-05 (One API 2.3.0: x-trace-id must be a UUID; executions actions) --> Every execution on PlungeAI is observable at four altitudes: live events while it runs, an execution record after it finishes, a persisted span-level trace, and the gateway's own request log with cost. Debugging is reading these in order — not re-running the workload to "see what happens". ## Correlation ids — wire them in from the start - Every One API response echoes a server-generated **`x-request-id`**. - Pass your own **`x-trace-id`** header (a **UUID** — anything else is ignored) on One API calls to correlate a whole operation across planes (an agent execute + a result redeem + a model call under one trace). **Fallback rule:** if you never sent `x-trace-id`, the trace id IS that request's `x-request-id` — so a call you did not pre-correlate is still traceable: take the echoed `x-request-id` and use it in `GET /v1/traces/{id}`. - Every execution acknowledgement carries `workflow_id` (the execution id) and `request_id`. Log them; they are the keys to everything below. ## Live: SSE event stream Streaming execution (`POST /v1/workflows/{id}/execute-stream`, and MCP streamed runs) emits server-timestamped events in order: ``` request_received → workflow_loaded → workflow_started → task_dispatched (per task) → task_completed (per task) → workflow_completed → workflow_result ``` Per-task duration = that task's `task_completed` − `task_dispatched`, using the SERVER timestamps — client network jitter is excluded. This event stream is the ground truth for performance questions ("which branch was slow"); never time a run from your own clock around the request. ## After the run: status and history ### `plungeai_get_workflow_status {execution_id}` Structured status: `status`, `workflow_name`, `started_at`, `duration_ms`, `final_task_id`, `error_message` (non-null on failure), and — crucially — `continuation`: non-null when the run is PAUSED awaiting the user (an `ask_user` question or an approval gate), carrying the agent, the question, and any pending action summary (with price when it is a money action). Relay that to the user and resume with `plungeai_continue`. The status check also **self-heals stuck runs** — poll it before declaring a run dead. Full parameter/return contract: `plungeai-workflows` (`references/mcp.md`). ### `plungeai_executions` — the run ledger `action: list|get|output|conversation|delete|cancel|steer|files` (the REST twin is `/v1/executions`). `list` is the "what ran lately" view; `get` the record; `output`/`conversation` the content (final, user-ready markdown — relay verbatim). This is also your idempotency check: before re-firing anything with side effects, look here for what already ran. Full parameter/return contract: `plungeai-workflows` (`references/mcp.md`). ### Execution summary (per run) Every engine run reports: `total_tasks_executed`, `tasks_skipped`, `conditions_evaluated`, `nesting_levels_processed`, `parallel_blocks_executed`, `sequential_blocks_executed`, per-task durations (with attempts), and **`failed_branches[]`** — parallel branches that failed WITHOUT aborting the run. The key is present only when something was lost; its absence is the all-clear. Always check it before trusting a synthesis built over a fan-out. ## Deep: persisted traces ```bash curl -s https://api.plungeai.com/v1/traces/{traceId} \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` Returns two arrays: - **`spans`** — engine execution spans: `type` (`request_received`, `task_dispatched`, `task_completed`, `workflow_result`, …), `workflow_id`, `task_id`, `agent`, `status`, `duration_ms`, timestamp. Large payloads are stored by reference (`payload_ref`); small ones inline (`payload_inline`, guard-scanned, ≤1 KB) — traces never leak megabytes into your context. - **`gateway_requests`** — the router's own log per request: `plane`, `route`, `status`, `duration_ms`, and **`cost_usd`** where priced. This is where "what did that run cost" is answered; model usage is captured per task and priced post-hoc from the platform's pricing catalog. Full route detail (parameters, error shapes, the outbound-MCP variant of tracing): [`references/traces.md`](/skills/plungeai-results-traces/references/traces). ## Scheduler observability Scheduled work has its own ledger on top (see `plungeai-scheduling`): `plungeai_schedule {action: "stats"}` for the fleet (active/paused jobs, today's success/failure split, average duration) and `{action: "runs", job_id}` for one job's history — whose Execution ID column bridges into everything above. ## Quality signals in discovery Registry search accepts `include=quality`: each card gains `{score, success_rate, runs}` (null when unmeasured). Use it when choosing between similar agents — measured success beats description prose. ## The debugging playbook 1. **Run seems hung** → `plungeai_get_workflow_status` (self-heals; may return a `continuation` — the run is waiting on a human, not hung). 2. **Run failed** → status `error_message` → the trace's failing span (`status`, `agent`, `duration_ms`) → that agent's outcome remediation. 3. **Run "succeeded" but output is thin** → execution summary `failed_branches[]` and `tasks_skipped` (a false condition silently skips tasks — that is a feature, verify the condition). 4. **Slow** → per-task durations from events/spans. Parallel block cost = slowest child; a staircase of child durations against one destination means queueing at that service — spread destinations, don't widen the block (`plungeai-workflows`). 5. **Expensive** → `gateway_requests.cost_usd` by route + the run's per-task model usage; then cap with `effort`/`max_turns` (missions, `plungeai-missions`) or a cheaper `model` (`plungeai-models`). 6. **Behaved as if instructions were missing** → the run's warnings: capability ids that failed to resolve or were deferred over budget (`plungeai-skills-plugins`). ## What to log in YOUR integration Minimum for a production integration calling the platform: the `x-request-id` of every call, the `workflow_id`/`execution_id` of every run you start, and your own `x-trace-id` per user-visible operation. With those three, every incident is reconstructable from the platform side; without them you are grepping timestamps. # Traces — GET /v1/traces/{id} Source: https://www.plungeai.com/docs/skills/plungeai-results-traces/references/traces <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/traces.ts, orchestration/api-gateway/api-gateway.ts | last-synced: 2026-10-05 (One API 2.3.0: a trace is owner-only, unknown and foreign ids are 404; x-trace-id must be a UUID) --> The observability plane: one id follows a request across every internal hop. (This is the traces half of the One API's discovery+traces route group — for `/v1/discovery/*` see `plungeai-discovery`.) Auth: `Authorization: Bearer ozk_YOUR_KEY` on every route here. ## GET /v1/traces/{id} — the execution trace Every response from the API carries a server-minted `x-request-id`; that id is also a trace id. To correlate **multiple** calls (or to pick the id yourself), send an `x-trace-id` header and **keep your own copy** — it is threaded through every internal hop of every plane, but it is **not echoed on the response** (only `x-request-id` is). The header must be a **UUID**: any other value is ignored and the trace id falls back to that call's `x-request-id`. ```bash # 1. Execute with your own trace id (a UUID) TRACE=$(uuidgen | tr 'A-Z' 'a-z') curl -s -X POST https://api.plungeai.com/v1/agents/one-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "x-trace-id: $TRACE" \ -d '{"prompt": "Say: traced"}' # 2. Read the whole story curl -s https://api.plungeai.com/v1/traces/$TRACE \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "trace_id": "00000000-0000-4000-8000-000000000006", "spans": [ { "id": 101, "trace_id": "00000000-0000-4000-8000-000000000006", "workflow_id": "2cecae7f-...", "task_id": null, "ts": 1787706891000, "type": "request_received", "agent": null, "status": null, "duration_ms": null, "payload_ref": null, "payload_inline": "..." }, { "id": 102, "trace_id": "00000000-0000-4000-8000-000000000006", "workflow_id": "2cecae7f-...", "task_id": "t1", "ts": 1787706891200, "type": "task_dispatched", "agent": "one-agent", "status": null, "duration_ms": null, "payload_ref": null, "payload_inline": null }, { "id": 103, "trace_id": "00000000-0000-4000-8000-000000000006", "workflow_id": "2cecae7f-...", "task_id": "t1", "ts": 1787706892100, "type": "task_completed", "agent": "one-agent", "status": "success", "duration_ms": 900, "payload_ref": null, "payload_inline": "traced" }, { "id": 104, "trace_id": "00000000-0000-4000-8000-000000000006", "workflow_id": "2cecae7f-...", "task_id": null, "ts": 1787706892150, "type": "workflow_result", "agent": null, "status": "success", "duration_ms": 950, "payload_ref": null, "payload_inline": "traced" } ], "gateway_requests": [ { "id": "9f2c1e44-...", "trace_id": "00000000-0000-4000-8000-000000000006", "plane": "agents", "route": "/v1/agents/:id/execute", "user_id": "u-...", "status": 200, "upstream": "cnl-engine", "duration_ms": 1180, "cost_usd": null, "created_at": 1787706892 } ] } ``` Reading it: - **Span sequence**: `request_received → workflow_loaded → workflow_started → task_dispatched → task_completed → workflow_result`. A task with `retry` configured shows duplicate `task_dispatched` spans on the same `task_id`. - **Payloads**: `payload_inline` is guard-scanned text ≤1KB (secrets/PII come back redacted); larger payloads live behind `payload_ref` (a storage key, not a URL you can fetch through this API). - **`gateway_requests`** is the router's own request log for the same trace id — plane, route, status, upstream, latency, and cost where measured. - **Ownership and 404.** A trace is readable only by the account that owns it: an id that is unknown, or belongs to another key, is `404 not_found` — the same answer for both, so a stranger learns nothing. Spans are written asynchronously: a read right after a request may 404 or come back short for a few seconds. Retry for a short capped while, then stop. The route returns at most the first **500 spans**. Use traces to debug: a `502 engine_error` on execute + the trace for that request id shows exactly which hop failed and how long each took. The higher-altitude reads (live SSE events, execution summary, the debugging playbook) live in [`references/observability.md`](/skills/plungeai-results-traces/references/observability). # plungeai-scheduling Source: https://www.plungeai.com/docs/skills/plungeai-scheduling Cron-schedule PlungeAI workflows, agent queries, pre-built agent cards, and condition-watching heartbeats via plungeai_schedule, with full run history and retries. Also one-time runs (`@once` + `run_at`), wake-ups, and webhook/email/outbound-hook triggers, over MCP, REST (`/v1/schedules`, `/v1/triggers`) and the CLI. Use when the user wants something to run daily/hourly/on a cron, once at a clock time, a recurring report, a standing watchdog, or asks about job status, retries, or why a scheduled run didn't fire. For the workflow or mission being scheduled use `plungeai-workflows` / `plungeai-missions`; for a list-to-completion campaign's own cadence use `plungeai-campaigns`. [Download zip](https://skills.plungeai.com/plungeai-scheduling.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-scheduling/SKILL.md) The scheduler runs platform work unattended on cron expressions: a saved workflow every morning, an agent query hourly, a pre-built agent card nightly, or a condition-watching heartbeat. Every job has first-class run history — nothing here is fire-and-forget. ## Prerequisites - A self-service `ozk_` API key from **Dashboard → One API → Keys** (https://dashboard.plungeai.com), or an MCP client connected to `https://mcp.plungeai.com/v1`. - The thing being scheduled must already exist and be discoverable: `plungeai_list_workflows` for a saved workflow, `plungeai_list_agents {search: "…"}` for an agent/check_agent id — never guess an id, it is fence-checked at create time. ## Creating a job — `plungeai_schedule` Action-routed, one tool: ``` plungeai_schedule {action: "create", name: "Daily market brief", job_type: "workflow", # agent | query | workflow | heartbeat target: "<workflow id>", schedule: "0 7 * * *"} # 07:00 UTC daily plungeai_schedule {action: "list"} plungeai_schedule {action: "run_now", job_id: "…"} # always do this right after create plungeai_schedule {action: "runs", job_id: "…"} # Execution ID column → plungeai_get_result plungeai_schedule {action: "stats"} ``` Four `create` shapes — workflow (`target` = a workflow id you own), agent (`target` = an agent id + `parameters: {prompt}`), query (`target` = `{agent, query}` JSON), and heartbeat (`check_agent` or `check_workflow` + `condition_prompt`, ticks and only acts when the condition is met — of the six `notify_channel` values, telegram/discord/slack/teams/email all deliver through the normal per-channel adapters; whatsapp is a known gap, prefer `trigger_workflow` or another channel for it). `mission_ref` on create schedules a pre-built agent card directly, resolved live at fire time. Standard 5-field cron (UTC unless the job carries an IANA `timezone`), 1-minute minimum granularity, ±5s accuracy. Use the rolling date tokens (`{yesterday}`, `{week_start}`, `{last_week_end}`, …) instead of baking concrete dates into scheduled YAML — full table in [`references/scheduling.md`](/skills/plungeai-scheduling/references/scheduling). ## One-time runs, wake-ups, triggers - **One run at a clock time:** `schedule: "@once"` plus `run_at` — an ISO-8601 time WITH a UTC offset (`2026-10-06T09:00:00Z`), in the future. Without it the create is refused (`run_at required` on MCP, `400 run_at_required` on REST; a past time is `400 run_at_past`). The job fires once and removes itself; its history stays. `ends_at` (ISO-8601) stops a recurring job after a date. - **Wake:** fire a saved workflow or bot once without touching its schedule — a pending wake is re-timed, never stacked. `note` becomes the run's input. REST also takes `in` (`"10m"`) to delay it; MCP and CLI wake on the scheduler's next tick. A heartbeat has no saved workflow to wake. - **Triggers** start a schedule's workflow from outside: a `webhook` (an HTTPS URL; schemes generic, github, gitlab, standard-webhooks; `prompt_template` with `{dotted.path}` fields; `filters`), an `email` address (`bot+<token>`, optional `allowed_from`), and outbound `hooks` (signed `run.started | run.finished | run.failed | run.needs_approval` events sent to your https URL). Mint, list, revoke; on REST only, webhook and email also rotate (`/{token}/rotate`). A secret is shown once, on mint. | | MCP `plungeai_schedule` | REST (`ozk_` key) | CLI (`ocean-cli`) | |---|---|---|---| | one-time | `create` with `schedule: "@once"`, `run_at`, `ends_at` | `POST /v1/schedules` | `schedules create NAME --type workflow --target <workflow id> --cron @once --at <iso> [--ends-at <iso>]` | | wake | `wake` (`job_id`, `note`) | `POST /v1/schedules/{id}/wake` | `schedules wake <job_id> --note "…"` | | webhook | `webhook_mint` / `webhook_list` / `webhook_delete` (`job_id`; delete takes `trigger_id`) | `/v1/triggers/webhook` (+ `/{token}`, `/{token}/rotate`) | `schedules triggers webhook mint <job_id> [--scheme --prompt-template --filters]\|list <job_id>\|delete <job_id> <token> --yes` | | email | `email_mint` / `email_list` / `email_delete` | `/v1/triggers/email` (same shape) | `schedules triggers email mint <job_id> [--allowed-from]` (+ list, delete) | | outbound hooks | `hooks_mint` / `hooks_list` / `hooks_delete` | `/v1/triggers/hooks` (+ `/{id}`) | `schedules triggers hooks mint <job_id> --url <https> [--events]` (+ list, delete) | REST creates only `workflow` and `heartbeat` schedules (wrap an agent in a saved workflow first); every schedule route also lists runs (`GET /v1/schedules/{id}/runs`), pauses, resumes and runs now (`{run_id, execution_id}`; `execution_id` is what `plungeai-results-traces` takes). Lists take `limit` 1–100 (default 20) and `offset`; `GET /v1/schedules` filters by `status`, `job_type` and `workflow_id`. A bot schedules itself with its own `schedule` tool (`plungeai-bot-agent`). Route and field detail: `plungeai-api-setup` → `references/bots-and-door.md`; this skill's [`references/scheduling.md`](/skills/plungeai-scheduling/references/scheduling). ## Verify every job you create `run_now` fires immediately and reports the REAL outcome plus a resolvable execution id — always call it right after `create` to prove the job works end-to-end, rather than waiting for the first natural fire. ## Gotchas - **Workflow jobs run by reference** — edit the workflow in Studio and the next scheduled run uses the latest saved version automatically; there is no "update the schedule to change the logic". - **Delete is soft** — history stays readable, but a deleted job refuses run_now/update/pause/resume; there is no undelete. - Failed runs auto-retry (3 attempts, 5 minutes apart) and **re-fire the FULL job** — schedule only idempotent work, or make the workflow itself idempotent. - A scheduler *run id* (`exec_...`, underscore) is not resolvable by `plungeai_get_workflow_status` / `plungeai_get_result` directly — go through `action: "runs"` and take its Execution ID column first. - Scheduled user workflows keep the full per-user memory lifecycle (recall + write) — only system jobs and heartbeat checks are memory-free. - `@once` fires once at `run_at`, then soft-deletes; `runAgain` (a workflow's own last step) self-schedules the next `@once` run without any cron expression at all — the mechanism behind a campaign that "just keeps going." ## Related skills - `plungeai-workflows` — author the workflow a job runs. - `plungeai-missions` — author the mission a `mission_ref` job runs. - `plungeai-campaigns` — a list-to-completion ledger driven by cron or `runAgain`. - `plungeai-memory` — the recall/write lifecycle of a scheduled run. - `plungeai-results-traces` — read a job run's status, output, and trace once you have its Execution ID. ## Reference - [`references/scheduling.md`](/skills/plungeai-scheduling/references/scheduling) — full action table, cron/date-token reference, heartbeat semantics, retries/history, troubleshooting, and the `plungeai_schedule` MCP tool contract. ## Reference pages <CardGroup cols={2}> <Card title="Scheduling — cron jobs for workflows, agents, and missions" icon="file-text" href="/skills/plungeai-scheduling/references/scheduling"> The scheduler runs platform work unattended on cron expressions: a saved workflow every morning, an agent query hourly, a pre-built agent card nightly. </Card> </CardGroup> # Scheduling — cron jobs for workflows, agents, and missions Source: https://www.plungeai.com/docs/skills/plungeai-scheduling/references/scheduling <!-- sources-of-truth: orchestration/scheduler/README.md, orchestration/scheduler/CLAUDE.md, orchestration/scheduler/SchedulerDO.ts (heartbeat delivery), orchestration/scheduler/delivery.ts, orchestration/mcp-gateway/server.ts, orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/extras.ts, orchestration/api-gateway/openapi-schedules.ts, orchestration/cnl-engine/README.md (date tokens) | last-synced: 2026-10-05 (run_at, wake, triggers; every verb, field and status re-checked against the MCP schema, the CLI help and GET /v1/openapi.json) --> The scheduler runs platform work unattended on cron expressions: a saved workflow every morning, an agent query hourly, a pre-built agent card nightly. Jobs, run history, retries, and stats are first-class — a scheduled job is not fire-and-forget; every run is logged and resolvable like any other execution. ## Creating and managing jobs — `plungeai_schedule` One MCP tool, action-routed: ``` plungeai_schedule {action: "create", name: "Daily market brief", job_type: "workflow", # agent | query | workflow | heartbeat target: "<workflow id>", schedule: "0 7 * * *" # 07:00 UTC daily } plungeai_schedule {action: "list"} plungeai_schedule {action: "get", job_id: "…"} plungeai_schedule {action: "update", job_id: "…", schedule: "0 8 * * 1-5"} plungeai_schedule {action: "pause", job_id: "…"} plungeai_schedule {action: "resume", job_id: "…"} plungeai_schedule {action: "run_now", job_id: "…"} # returns the run's REAL outcome + execution id plungeai_schedule {action: "runs", job_id: "…"} # execution history plungeai_schedule {action: "stats"} plungeai_schedule {action: "delete", job_id: "…"} ``` Also on create: `mission_ref` schedules a pre-built agent card directly (a bounded mission on cron — see `plungeai-missions`), and `parameters` carries the run input. ### Heartbeat jobs — watch a condition, act only when it trips `job_type: "heartbeat"` is a user-facing job kind (not just internal plumbing): each tick it runs `check_agent` against `condition_prompt` (or `check_workflow`: a saved workflow whose final task answers YES or NO) and only acts when the condition is met. Create requires `name`, `schedule`, `check_agent` or `check_workflow`, `condition_prompt`; optional actions: `trigger_workflow` (a workflow id to fire) and `notify_channel` (`telegram | whatsapp | discord | slack | teams | email`) + `notify_chat_id`. Heartbeat notify routes through the same per-channel delivery adapters as ordinary scheduled-result delivery — telegram/discord/slack/teams/email all deliver (subject to each channel's normal pairing/confirmation fence, see `plungeai-campaigns`' delivery-channel table); **whatsapp has a known gap** — the heartbeat payload never carries the `phone_number_id` the whatsapp adapter requires, so a whatsapp target silently no-ops today. ``` plungeai_schedule {action: "create", job_type: "heartbeat", name: "Price watch", schedule: "*/30 * * * *", check_agent: "<agent id from a live search>", condition_prompt: "Is BTC below $50k? Answer MET or NOT MET with evidence.", notify_channel: "telegram", notify_chat_id: "…"} ``` Heartbeat condition checks run memory-free — they are watch-and-notify ticks, not learning runs. Semantics you should rely on: - **Workflow jobs run by reference** — the scheduler stores a pointer, not a YAML copy. Edit the workflow in Studio and the next scheduled run uses the latest version automatically. Never "update the schedule" to change workflow logic. - **Agent and query jobs are auto-wrapped at create time** into a saved one-task workflow (`Scheduled: <name>`) and scheduled as workflow jobs — so their runs get full standard execution history too. - **Delete is soft**: history stays readable; a deleted job refuses run_now/update/pause/resume. There is no undelete — create a new job. - **`run_now` returns the real outcome** plus a resolvable execution id — use it to verify a job end-to-end right after creating it (always do this). ## Cron expressions Standard 5-field cron, minimum granularity **1 minute**, evaluated in UTC unless the job carries an IANA `timezone` (`plungeai_schedule` `timezone`, REST `timezone`): ``` minute hour day month day-of-week ``` | Intent | Expression | |---|---| | Every 5 minutes | `*/5 * * * *` | | Hourly on the hour | `0 * * * *` | | Daily 07:00 | `0 7 * * *` | | Weekdays 09:00 | `0 9 * * 1-5` | | Mondays 09:00 | `0 9 * * 1` | | 1st of month 00:00 | `0 0 1 * *` | `,` lists, `-` ranges, `/` steps, `*` any. Cron accuracy is ±5 seconds — do not build designs that need tighter timing. ### `@once` and `runAgain` — one-time and self-scheduling jobs `@once` is a valid schedule that fires exactly once, then soft-deletes itself (run history survives). Give the time as `run_at`: ISO-8601 WITH a UTC offset, in the future (`plungeai_schedule` create: refused with `run_at required` without it; REST: `400 run_at_required` / `400 run_at_past`; CLI: `schedules create --cron @once --at <iso>`). A failed `@once` run may retry once with the standard +5 min delay before its final deletion. `ends_at` on a recurring job stops it after that date. `runAgain` is the mechanic behind a **campaign** that re-schedules itself without a cron expression: the last row of its per-cycle pipeline is `agent: scheduler, params: {operation: "runAgain", in: "1h"}`. It creates exactly ONE `@once` job for that workflow (never stacks — an existing pending `@once` job for the same workflow is re-timed, not duplicated). `in` accepts a duration (`5m`, `1h`, `90s`) or a number of seconds, default `1h`. This is orthogonal to a campaign scheduled *with* a cron expression, which is driven by an ordinary recurring job instead — see `plungeai-campaigns`. ## Date tokens — schedules that reason about time windows Scheduled workflows should never bake concrete dates into YAML. The engine seeds rolling tokens from each run's execution time — same `{placeholder}` syntax as inputs, lower precedence than caller inputs: `{now}` `{today}` `{yesterday}` `{week_start}` `{week_end}` `{last_week_start}` `{last_week_end}` `{month_start}` `{month_end}` `{last_month_start}` `{last_month_end}` — all UTC, ISO weeks (Monday = day 1). ```yaml - type: task id: compare agent: one-agent prompt: | Compare this week ({week_start} to {week_end}) against last week ({last_week_start} to {last_week_end}). Identify the notable moves. ``` The scheduler passes the execution time automatically — a job scheduled "Monday 09:00" reasons about the correct ISO week every time, forever, with zero YAML edits. ## Runs, retries, and history - Every run is logged: status (`running` → `completed`/`failed`), start/end, duration, result pointer, error message, attempt number, what triggered it (schedule vs manual). - **Failed runs auto-retry**: up to 3 attempts by default, 5 minutes apart; after max retries the job returns to its normal schedule (it is not paused by failure). The per-job `max_retries` override is not a field of `plungeai_schedule` or of `POST /v1/schedules`. - Retry re-fires the full job — schedule only idempotent work, or make the workflow itself idempotent (e.g. keyed upserts), for anything with side effects. - `action: "runs"` lists history; its **Execution ID column** is the id to fetch output with `plungeai_get_result` / inspect with `plungeai_get_workflow_status`. A scheduler *run id* itself is NOT resolvable by those tools — both will tell you to go through `runs` first; do that instead of retrying. - `action: "stats"` summarizes: total/active/paused jobs, today's runs and success/failure split, average duration — the first read when "schedules seem broken". ## Memory semantics of scheduled runs A scheduled user workflow keeps the FULL per-user memory lifecycle — it recalls and writes long-term memory exactly like a manual run (owner decision; see `plungeai-memory`). Only system jobs and heartbeat condition checks run memory-free. Design accordingly: a daily mission genuinely accumulates knowledge run over run. ## Patterns - **Morning brief:** workflow (parallel news/search fan-out → synthesis) on `0 7 * * *`, date tokens for "since yesterday". - **Weekly comparison report:** workflow with `{last_week_*}` vs `{week_*}` windows on `0 9 * * 1`. - **Standing watchdog:** `quick`-effort mission card via `mission_ref` hourly; it checks a condition and only escalates (writes/notifies) when triggered. - **Data hygiene:** batch workflow nightly over a table; opt in with `track: true` on the batch so re-runs resume instead of repeat (`plungeai-workflows`). - **Campaign cadence:** a list-to-completion campaign is driven either by an ordinary recurring cron job or by its own `runAgain` self-scheduling — see `plungeai-campaigns`. ## Troubleshooting 1. Job not firing → `get` the job: `status` must be `active` and next run in the future; then `runs` for the last attempt's error. 2. Run failed → `runs` → take the Execution ID → `plungeai_get_workflow_status` (which also self-heals stuck runs) and the trace (`plungeai-results-traces`). 3. Wrong data window → check the zone: `0 7 * * *` is 07:00 UTC unless the job has a `timezone`; date tokens are UTC/ISO either way. 4. Output looks stale → the job runs the workflow by reference; confirm which version is saved (someone may have edited it — that IS the version that runs). --- ## MCP tool: `plungeai_schedule` **Purpose:** cron-scheduled jobs — run agents, workflows, or condition-check heartbeats on a schedule. Jobs are ownership-scoped. **Actions** | Action | Requires | Notes | |---|---|---| | `stats` | — | Job counts by status. | | `list` | — | Table: Name, Type, Schedule, Status, Next run, ID. | | `get` | `job_id` | Full job JSON. | | `create` | see below | Four job shapes (below). | | `update` | `job_id` (+ any of name/description/schedule/timezone/target/parameters/ends_at) | Deleted jobs refuse it. | | `pause` / `resume` | `job_id` | Toggle `paused`/`active`. | | `delete` | `job_id` | SOFT delete — run history stays readable via `runs`; deleted jobs refuse run_now/update/pause/resume. | | `run_now` | `job_id` | Fire immediately; reports the run's REAL outcome and a resolvable execution id. | | `runs` | optional `job_id` | Run history table; its **Execution ID** column is what `plungeai_get_result` accepts. | | `wake` | `job_id` (+ `note`) | Fire the schedule's saved workflow once, on the next tick (MCP 2.6.0). | | `webhook_*` / `email_*` / `hooks_*` | `job_id` (+ `mint`: trigger fields; `delete`: `trigger_id`) | Mint, list, revoke a schedule's triggers (MCP 2.6.0). | **Create — the four shapes** 1. **Workflow job:** `job_type: "workflow"`, `target: "<workflow_id you own>"`, `name`, `schedule` (cron). The workflow must exist in the caller's account. 2. **Agent job:** `job_type: "agent"`, `target: "<agent-id>"`, `parameters: {"prompt": "what to do each run"}`, `name`, `schedule`. The agent is fence-checked NOW (unknown/inactive refused at create, not at fire time), and the job is auto-wrapped into a visible saved workflow ("Scheduled: <name>") so runs land in normal execution history. 3. **Query job:** `job_type: "query"`, `target` is JSON: `{"agent": "<agent-id>", "query": "<what to run>"}` — same wrapping and fence as agent jobs. 4. **Heartbeat:** `job_type: "heartbeat"`, plus `check_agent` (or `check_workflow`, a saved workflow whose final task answers YES or NO) and `condition_prompt` (required), optional `trigger_workflow` (workflow id to fire when the condition is met), `notify_channel` (`telegram|whatsapp|discord|slack|teams|email`) + `notify_chat_id`. Each tick it checks the condition and only acts when met. Delivery runs through the same per-channel adapters as regular scheduled-result delivery — telegram, discord, slack, teams, and email all deliver; ⚠️ **whatsapp is a known gap** (the heartbeat payload doesn't carry the `phone_number_id` the adapter requires, so it silently no-ops) — prefer `trigger_workflow` or another channel for a whatsapp-bound notice. Also on create: `mission_ref: "<agent-card-id>"` (+ `name`, `schedule`, optional `parameters.prompt`) schedules a pre-built agent card — the card resolves LIVE at fire time, so card edits apply to future runs automatically. **One-time and wake (MCP 2.6.0)** - `create` with `schedule: "@once"` also needs `run_at` (ISO-8601 with a UTC offset, in the future); without it the answer is `run_at required`. `ends_at` (ISO-8601; create or update) stops a recurring job after that date. `timezone` is the IANA zone the cron is read in. - `wake` (`job_id` of the schedule, optional `note` that becomes the run's input) fires the schedule's saved workflow once, on the scheduler's next tick, without touching the schedule; a pending wake is re-timed, never stacked. A heartbeat cannot be woken. (REST `POST /v1/schedules/{id}/wake` also takes `in` to delay it.) - `webhook_mint | webhook_list | webhook_delete`, `email_mint | email_list | email_delete`, `hooks_mint | hooks_list | hooks_delete` manage a schedule's triggers (`job_id` names the schedule). Mint takes the trigger's fields (webhook: `scheme`, `prompt_template`, `filters`; email: `allowed_from`; hooks: `url`, `events`); `*_delete` takes `trigger_id` (the token, or the hook id) from the list. The secret is returned once and never listed. Same operations as REST `/v1/triggers/{webhook|email|hooks}`. **Example** ```json {"user_request": "every weekday at 7am send me an AI news brief", "action": "create", "name": "Morning AI brief", "job_type": "agent", "target": "brave-agent", "schedule": "0 7 * * 1-5", "parameters": {"prompt": "Top AI news of the last 24h, with links"}} ``` **Failures & fixes** - "create requires: name, job_type (agent|query|workflow), target, schedule (cron)" → supply them (heartbeat/mission_ref list their own requireds). - Fence refusal on `target`/`check_agent` → re-discover with `plungeai_list_agents`; never retry the same id. - "Agent schedules need a prompt" → add `parameters: {"prompt": ...}`. - "No workflow `<id>` found in your account" → pass an id from `plungeai_list_workflows` or build one first. - "That looks like an execution id (`exec_...`), not a job id (`job_...`)" → execution ids identify single runs; use `action: "runs"` to list them, or pass the job id. - "The scheduler's storage is briefly busy... Retry in a few seconds." → transient; the job most likely still exists — do NOT treat as not-found. - `Scheduler error (HTTP <status>).` (e.g. `Scheduler error (HTTP 401).`) → the gateway→scheduler hop itself failed; not your arguments. Report it rather than retry-looping. - To fetch a scheduled run's output: `action: "runs"` → take the **Execution ID** column → `plungeai_get_result {workflow_id: <that id>}`. # plungeai-skills-plugins Source: https://www.plungeai.com/docs/skills/plungeai-skills-plugins Declare and understand PlungeAI's capability-injection fields on a type: harness task or plungeai_run_mission — skills, experts, persona, backgrounds, plugins, and MCP servers — including eager-vs-deferred budgets and degrade-to-warning failure semantics, how to look the ids up over REST (`GET /v1/discovery/search?kind=…`), and how to save your own skill (`POST /v1/skills/learn`). Use when the user wants an agent to follow a house style or method, adopt an identity/voice, get standing company context, or wire in a domain toolkit or MCP server. For authoring the mission itself use `plungeai-missions`; for saving a skill from this chat use `plungeai-memory`'s plungeai_learn. [Download zip](https://skills.plungeai.com/plungeai-skills-plugins.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-skills-plugins/SKILL.md) Six capability kinds shape a `type: harness` task (or `plungeai_run_mission`) beyond its goal: **skills** (methods — HOW), **experts** and **personas** (identity — WHO), **backgrounds** (ambient truth), **plugins** (bundled domain toolkits), and **mcp** (connected servers). All six are resolved by one enrichment pass at run start. ## Prerequisites - A self-service `ozk_` API key from **Dashboard → One API → Keys** (https://dashboard.plungeai.com), or an MCP client connected to `https://mcp.plungeai.com/v1`. - **Discover every id before declaring it** — a missing id never fails the run, it degrades to a silent warning: `plungeai_list_agents {kind: "skills"|"experts"| "personas"|"connectors", search: "<topic>"}` (REST: `GET /v1/discovery/search?kind=…&q=…`). Never guess. ## Declaring capabilities ```yaml - type: harness agent: one-agent goal: "Build a valuation snapshot for {input}" mission: | You are a buy-side analyst. persona: analyst experts: [securities-law] skills: [dcf-modeling-method] backgrounds: [acme-corp] plugins: [finance-bundle] mcp: [daloopa] ``` Injection order into the system prompt is fixed: `BACKGROUND` (verbatim, first) → persona text → `EXPERT` sections → skill bodies under `ADDITIONAL INSTRUCTIONS` → free-text `instructions:` (always appended, last word). ## The eager budget — the thing that trips people up All eager identity/instruction text shares **one 24 KB budget**: at most 3 backgrounds, 1 persona, 3 experts, 5 skills load eagerly; the rest **defer** to an `## AVAILABLE ON DEMAND` index the agent can pull mid-run with `load_skill`. Order your lists by importance — must-follow items go first or into `instructions:` (which always lands, never defers). A plugin's bundled skills are always indexed, never eagerly injected, regardless of count — see [`references/plugins.md`](/skills/plungeai-skills-plugins/references/plugins). ## Which to declare | You have | Declare | |---|---| | One instruction pack (method, house style) | `skills: [id]` | | An identity/voice for the whole run | `persona: id` (exactly one) | | A domain judgment lens | `experts: [id]` (up to 3 eager) | | Standing company/product context for every run in a family | `backgrounds: [id]` | | A coherent toolkit (several skills + scripts, versioned together) | `plugins: [id]` | | One MCP server the agent should call directly | `mcp: [server-id]` — plugins never add this for you | Do not unbundle a plugin into individual `skills:` entries to force eager injection — you lose its script index and blow the shared budget. A plugin's `.mcp.json` / manifest `mcpServers` documents the connectors its skills were written for; it is NOT auto-connected — declare that server's id yourself in `mcp:` alongside the plugin ([`references/plugins.md`](/skills/plungeai-skills-plugins/references/plugins)). ## Over the One API A `type: harness` task rides the workflows plane; the One API has no separate capability route. Same `ozk_` key (`plungeai-api-setup`). | Job | REST | MCP | CLI `ocean-cli …` | |---|---|---|---| | find an id | `GET /v1/discovery/search?kind=skills\|experts\|personas\|backgrounds\|plugins\|connectors\|mcp-servers&q=` | `plungeai_list_agents {kind, search}` (kinds: `skills`, `experts`, `personas`, `connectors`, …) | `discovery search Q --kind skills` | | read a card | `GET /v1/discovery/cards/{type}/{id}` (`skill`, `expert`, `digital_twin`, `background`, `plugin`, `mcp_server`, `connector`) | `plungeai_list_agents {agent_id}` | `discovery card TYPE ID` | | declare them | a `type: harness` task in `POST /v1/workflows/execute`, in a `workflow:` on `POST /v1/execute`, or saved (`POST /v1/workflows`, `kind: agent`) | `plungeai_execute_workflow`; `plungeai_run_mission` takes only `persona` and `skills` | `missions run --persona --skills`; the rest in a workflow YAML | | your own skills | `GET /v1/skills` → `{skills: [{id, description}], count}`; `POST /v1/skills/learn {name, body, description?}` → `201`; `DELETE /v1/skills/{id}` → `204` | `plungeai_learn` `list` / `learn` / `forget` | `learn list` / `add` / `forget` | What builders get wrong: - **`POST /v1/agents/{id}/execute` forwards only `persona`.** `skills`, `experts`, `backgrounds`, `plugins` and `mcp` sent there are ignored without an error: use a `type: harness` task for them. - **`GET /v1/skills` lists only the skills you saved.** Platform skills, experts, personas and plugins come from discovery; a saved skill is injected like any other with `skills: [<name>]`. `POST /v1/skills/learn` stores the markdown you send (`409 skill_name_taken` for a platform skill's name) — distilling a source into a skill is `plungeai_learn`. - Discovery kinds `personas` and `digital-twins` both return persona cards; `mcp-servers` and `connectors` list the servers a mission's `mcp:` can name. An MCP server that needs a credential the user has not connected answers a structured `needs_connection:<provider>` inside the run, not a failed request. ## Gotchas - Arrays REPLACE, never union, across mission-card merge (last wins: card → workflow root → task) — a task-level `experts: [x]` replaces the card's whole list. - Only the FIRST entry of `personas: [x]` (plural) is used — you never get a blend. - Missing/bad ids degrade to a warning, never a hard failure — a run that "lost its voice" or ignored a skill almost always has one in its warnings (`plungeai-results-traces`). - A plugin declaration never fails the run even if it can't be expanded — a bad id degrades to a warning and the plugin's skills/scripts are simply absent. ## Related skills - `plungeai-missions` — the harness task these fields attach to, and the merge order (card → workflow root → task). - `plungeai-memory` — `plungeai_learn` distills a chat session into a new skill. - `plungeai-results-traces` — where a degraded-capability warning shows up. - `plungeai-tools-connectors` — MCP server ids for the `mcp:` field. ## Reference - [`references/skills.md`](/skills/plungeai-skills-plugins/references/skills) — declaring/creating skills, the eager budget, debugging. - [`references/plugins.md`](/skills/plungeai-skills-plugins/references/plugins) — plugin expansion (skill index, MCP servers, script index), failure semantics. - [`references/experts-personas.md`](/skills/plungeai-skills-plugins/references/experts-personas) — backgrounds, persona, experts: injection order, budgets, merge behavior. ## Reference pages <CardGroup cols={2}> <Card title="Experts, personas, backgrounds — shaping WHO the agent is in a run" icon="file-text" href="/skills/plungeai-skills-plugins/references/experts-personas"> Three capability kinds shape an agent's identity and framing (as opposed to skills, which shape its methods). </Card> <Card title="Plugins — bundles of skills, MCP servers, and scripts injected as one unit" icon="file-text" href="/skills/plungeai-skills-plugins/references/plugins"> A plugin is a Claude-Code-format bundle stored on the platform: a directory tree containing any number of SKILL.md skills, an optional .mcp.json (or manifest… </Card> <Card title="Skills — injectable instruction packs for agent runs" icon="file-text" href="/skills/plungeai-skills-plugins/references/skills"> A skill is a markdown instruction pack (the Agent Skills SKILL.md format: frontmatter name + description, then the body) stored on the platform and injected… </Card> </CardGroup> # Experts, personas, backgrounds — shaping WHO the agent is in a run Source: https://www.plungeai.com/docs/skills/plungeai-skills-plugins/references/experts-personas <!-- sources-of-truth: core/core-base/pack-enrich.ts, orchestration/cnl-engine/harness-mission.ts, orchestration/cnl-engine/schema-types.ts | last-synced: 2026-09-24 --> Three capability kinds shape an agent's identity and framing (as opposed to skills, which shape its methods). All three inject into the **front** of the system prompt, in a fixed order, before anything else the run adds: ``` ## BACKGROUND: <id> ← ambient truth, first, verbatim <background body> <persona text> ← identity and voice ## EXPERT: <id> ← labeled domain lenses <expert body> ``` The order is load-bearing: backgrounds frame everything that follows, and putting them first keeps the prompt prefix stable across runs sharing a background (prompt caching). Experts are labeled sections precisely so the model can tell "domain lens" apart from "who I am". ## Backgrounds — always-on ambient context A **background** is a registry card whose body is standing context: company facts, product truth, an environment description. Injected FIRST and **verbatim** — no stripping — because ambient truth must arrive intact. ```yaml - type: harness agent: one-agent goal: "Draft the Q3 partner update" mission: | Write the quarterly partner update. backgrounds: [acme-corp] ``` Use a background when every run in a family needs the same grounding ("what Acme is, our products, our tone"), instead of pasting boilerplate into each mission text. Backgrounds are per-owner resolvable: your private background card wins for your runs. ## Persona — one voice per run A **persona** is an identity/voice text (from the platform's persona store). Exactly **one** per run — a run speaks with one voice. ```yaml persona: analyst ``` Accepted spellings, all normalized to the same thing: `persona:` (canonical), `digital_twin:` (alias), and `personas: [x]` (plural from preset frontmatter — only the FIRST entry is used). If you list several personas, you did not get a blend; you got `personas[0]`. Personas also appear outside missions: `plungeai_execute_agent`/`POST /v1/agents/{id}/execute` accept a `persona` parameter, and CNL `debate`/`validate` blocks take `persona`/`digital_twin` per debater, judge, and validator — the same store, applied per role. That is the idiomatic way to run a multi-perspective panel: one debate block, different personas per debater. ## Experts — labeled domain lenses (UI name: Specialists) An **expert** is deep domain instruction material injected as a labeled `## EXPERT: <id>` section — "reason like a securities lawyer", "apply SRE practices". Up to **3** load eagerly per run. ```yaml experts: [securities-law, python-pro] ``` Persona vs expert, the practical line: persona = who the agent IS (voice, identity — one). Expert = what the agent additionally KNOWS HOW to judge (lenses — up to three eager). A hedge-fund panel is personas in a debate; a compliance review is one persona plus a law expert. ## Budgets and degradation (shared with skills) All eager identity/instruction text — backgrounds + persona + experts + skills — shares **one 24 KB budget**, with per-kind caps: 3 backgrounds, 1 persona, 3 experts (5 skills). Over cap or over budget, items **defer**: the prompt lists them under `## AVAILABLE ON DEMAND` and the agent can pull one mid-run with `load_skill` (matching type). Persona is never deferred — it either resolves or warns. Missing ids never fail the run; they degrade to warnings: - `background "<id>" not found or empty` - `persona "<id>" not found` - `expert "<id>" not found` A run that "lost its voice" or ignored company context almost always has one of these warnings — check the run detail (`plungeai-results-traces`). The other classic cause: declaring more than the caps and assuming everything injected. Order lists by importance; the head injects, the tail defers. **Discover valid ids before declaring them** — the degrade-to-warning behavior means a guessed id fails silently: `plungeai_list_agents {kind: "experts", search: "…"}` or `{kind: "personas", search: "…"}` (REST: `GET /v1/discovery/search?kind=…&q=…`). ## Merge behavior in missions Like every mission field, these merge per key with **last-wins** across pre-built card → workflow root → task, and **arrays REPLACE rather than union** (see `plungeai-missions`). So a task-level `experts: [x]` replaces the card's expert list — and an explicit empty `experts: []` deliberately clears it. To ADD to a card's list you must restate the full list. ## Authoring guidance - Keep each body lean and self-contained: it lands in a prompt with everything else competing for 24 KB. A 15 KB background starves persona and experts. - Backgrounds: facts, not instructions. Instructions belong in mission text or skills — a background that says "always do X" fights the mission's own framing. - Experts: method and judgment criteria ("what a great X checks first"), not essays. - Test identity injection cheaply: run a `quick`-effort mission whose goal is to introduce itself and state its operating context; the answer shows exactly which layers landed. # Plugins — bundles of skills, MCP servers, and scripts injected as one unit Source: https://www.plungeai.com/docs/skills/plungeai-skills-plugins/references/plugins <!-- sources-of-truth: core/core-base/pack-enrich.ts (expandPlugin), orchestration/cnl-engine/schema-types.ts | last-synced: 2026-09-24 --> A **plugin** is a Claude-Code-format bundle stored on the platform: a directory tree containing any number of `SKILL.md` skills, an optional `.mcp.json` (or manifest `mcpServers`) documenting the MCP servers its skills were written for, and executable Python scripts. Declaring ONE plugin on a mission indexes the whole bundle's skills and scripts in a single id — its documented MCP servers are NOT auto-connected (below). Real plugins are big (a serious finance bundle ships a dozen skills, several scripts, and references to multiple MCP servers), which drives the design below: plugins are **indexed, never dumped**. ## Declaring a plugin ```yaml - type: harness agent: one-agent goal: "Build a valuation snapshot for {input}" mission: | You are a buy-side analyst. plugins: [finance-bundle] ``` Also accepted flat on the task alongside `skills`/`experts`/`mcp` (see `plungeai-missions` for the full authoring contract), and stored pre-built agent cards may declare plugins that merge into every run referencing the card. ## What expansion actually injects When the run starts, each declared plugin expands into two injected things (skills, scripts) plus one thing it deliberately does NOT do (auto-connect MCP servers): ### 1. A skill INDEX — progressive disclosure, deliberately Every bundled `SKILL.md` is indexed by **name + description only** (read from its frontmatter). The agent's prompt gets: ``` ## PLUGIN CAPABILITIES These skills come from the plugins in this mission. Only their names and descriptions are loaded. When one is relevant, read its full instructions FIRST with load_skill {type: "plugin_skill", plugin: "<plugin>", path: "<path>"} — then follow them. - **dcf-modeling** — Build a discounted-cash-flow model from … (plugin: finance-bundle · path: skills/dcf-modeling/SKILL.md) - … ``` Why an index and not the bodies: injecting bodies would burn the run's entire eager prompt budget (24 KB shared — see `skills.md`) on text the agent may never need, and an arbitrary-order "first five" cutoff would leave the rest unreachable. With the index, **every** bundled skill is discoverable and the agent pays for a body only when it decides it is relevant. There is no cap on index size. Consequence: a plugin skill influences the run ONLY if the agent loads it. If a particular bundled procedure MUST apply, say so in the mission text ("follow the dcf-modeling skill from the finance bundle") — the agent will then load it first. ### 2. MCP servers are NOT auto-connected — declare them yourself A plugin's `.mcp.json` / manifest `mcpServers` are the connectors its bundled skills were WRITTEN FOR, not an instruction to connect them: `expandPlugin` deliberately ignores them. An earlier version folded them into the run and silently connected 7–12 paid SaaS servers per plugin for runners who never authorized them (a wall of 401/`needs_connection` steps before the first turn) — so declaring a plugin injects only its skill index and script index (below); the run's live MCP set is the mission's own `mcp:` field alone. If a bundled skill needs a server, declare that server id yourself in `mcp:` alongside the plugin (see the skill's own text/card for which id to use — `plungeai-tools-connectors` for discovering connector ids). ### 3. A script index for `run_python` Every bundled `.py` file is indexed (never injected as text): ``` ## PLUGIN SCRIPTS Executable scripts from the plugins in this mission (run with run_python, referencing plugin + path): - finance-bundle: scripts/wacc.py ``` The agent executes them with its `run_python` tool, referencing plugin + path. This is how a plugin ships deterministic computation alongside instructions — prefer a bundled script over asking the model to do arithmetic. ## Failure semantics Plugin resolution never kills a run. Degradations surface as warnings: | Warning | Meaning | Fix | |---|---|---| | `plugin "<id>" not found in plugin-memory` | Bad id or unpublished bundle | Verify the id; publish the bundle | | `plugin "<id>" could not be expanded` | Retrieval failed mid-expand | Transient — re-run; check platform status | | `plugins declared but PLUGIN_MEMORY_SERVICE is not bound` | The executing surface has no plugin store | Run the mission on the standard engine surface | A run that "should have had the toolkit" but behaved bare almost always has one of these warnings in its detail — check `plungeai-results-traces` for where to read them. Because resolution degrades silently, look ids up before declaring them: MCP server ids for `mcp:` lists come from `plungeai_list_agents {kind: "connectors", search: "…"}` (see SKILL.md "Discovery first" for the other injectable kinds); plugin bundle ids live in plugin-memory — verify one cheaply by declaring it on a `quick` mission and checking the run's warnings. ## Plugins vs skills vs MCP — which to declare | You have | Declare | |---|---| | One instruction pack | `skills: [id]` — cheaper, eagerly injected | | An MCP server the agent should call | `mcp: [server-id]` directly — plugins never add this for you | | A coherent domain toolkit (several skills + scripts, maintained together; documented MCP servers to declare alongside it) | `plugins: [id]` — one id, whole skill/script kit, versioned as a unit | Do not unbundle a plugin into individual `skills:` entries to force eager injection — you lose the script index and blow the budget. State the must-follow skill in the mission text instead. If the bundle's skills call out an MCP server, add that server's id to `mcp:` yourself — declaring the plugin alone will not connect it. # Skills — injectable instruction packs for agent runs Source: https://www.plungeai.com/docs/skills/plungeai-skills-plugins/references/skills <!-- sources-of-truth: core/core-base/pack-enrich.ts, orchestration/mcp-gateway/server.ts, orchestration/HARNESS-README.md, apps/ocean-skills/README.md, orchestration/api-gateway/routes/skills.ts, orchestration/mcp-gateway/ops/learn.ts | last-synced: 2026-10-05 (One API 2.3.0: /v1/skills, agents-route persona-only note, install status) --> A **skill** is a markdown instruction pack (the Agent Skills `SKILL.md` format: frontmatter `name` + `description`, then the body) stored on the platform and injected into agent runs. Where a workflow tells an agent WHAT to do, a skill tells it HOW: a method, a house style, a domain procedure, distilled research. Skills are per-user unless published; the same mechanism also powers platform-provided skills. Skills are one of six capability kinds a mission can declare (`skills`, `experts`, `persona`, `backgrounds`, `plugins`, `mcp`) — all resolved by the same enrichment pass. This file covers skills; siblings cover the rest (`experts-personas.md`, `plugins.md`). ## Declaring skills on a run Flat on any `type: harness` task (or in `plungeai_run_mission`): ```yaml - type: harness agent: one-agent goal: "Audit {input} for compliance gaps" mission: | You are a meticulous compliance auditor. skills: [compliance-audit-method, house-style] ``` The engine resolves every declared id in ONE parallel pass and injects the bodies into the agent's prompt under `## ADDITIONAL INSTRUCTIONS` (frontmatter and heading noise stripped — only the instructions land). Mission-level free-text `instructions:` are appended after skill bodies, so author instructions always get the last word. ## The eager budget — why not everything is injected Injection is **budgeted**, deliberately: eager text across backgrounds + persona + experts + skills shares one cap (24 KB), and at most **5 skills** load eagerly (experts cap at 3, backgrounds at 3). Beyond the caps or the byte budget, skills are not dropped — they are **deferred**: the agent's prompt gets an `## AVAILABLE ON DEMAND` index listing them by id, and the agent pulls a body mid-run with its `load_skill` tool when it judges one relevant. Consequences for you: - **Order your `skills:` list by importance.** The first ids get eager injection; the tail defers. - Declaring 12 skills is legal but means 7+ are on-demand — fine for reference material, wrong for must-follow rules. Must-follow goes first or into `instructions:`. - A missing id does not fail the run: it degrades to a warning (`skill "<id>" not found`) and the mission runs without it. Check warnings when a run ignores instructions you thought you injected — the usual cause is a typo'd id silently degraded. - **Discover valid skill ids before declaring them**: `plungeai_list_agents {kind: "skills", search: "<topic>"}` (REST: `GET /v1/discovery/search?kind=skills&q=…`). Never guess an id — the degrade-to- warning behavior means a guessed id fails silently. ## Creating skills ### From a chat/agent session: `plungeai_learn` ``` plungeai_learn {source: "<a URL, pasted PDF text, or distilled findings from this chat>"} ``` Runs the platform's `learn` agent: reads the source, writes a lean SKILL.md body, and saves it **private to you**. Async by default — poll `plungeai_get_workflow_status`, fetch with `plungeai_get_result`. This is the "learn-back" move: when a session produced real research, offer to distill it so future runs can inject it. Full tool contract: `plungeai-memory`. ### Over the One API: `POST /v1/skills/learn` ```bash curl -s -X POST https://api.plungeai.com/v1/skills/learn \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"name": "compliance-audit-method", "body": "# Audit method\n\nAlways cite the clause. …", "description": "How we audit a vendor contract"}' # 201 {"skill": {"id": "compliance-audit-method", "description": "How we audit a vendor contract"}} ``` This saves the markdown you wrote as a private card (`name` kebab-case, 2–64 characters; `body` up to 65,536; `description` up to 60, default the body's first line). Saving a name you already own replaces it; a name held by a platform skill or another account is `409 skill_name_taken`. It does **not** distil a source — that is `plungeai_learn`. `GET /v1/skills` lists yours (`{skills: [{id, description}], count}`, platform skills are not listed) and `DELETE /v1/skills/{id}` removes one (`204`; `404 skill_not_found` for a platform or another account's skill; `502 registry_unavailable` means nothing was deleted). Inject it with `skills: [<name>]`. ### From inside a mission Loop agents carry a `skill_manage` tool — a mission can save or update a skill as part of its work (e.g. a nightly job that maintains a "current market context" skill). ### Quality bar for a skill body The injector strips frontmatter and `#` headings, so write bodies that survive that: imperative instructions, concrete rules, short examples. A skill that is one big heading outline injects as almost nothing. Keep bodies lean — remember they share a 24 KB budget with everything else; a 20 KB skill starves the rest of the pack. ## Using skills without a mission `one-agent` in call mode applies `persona`, `skills` and `experts` given as fields of a `type: task` in a workflow — the one-shot way to get skill-shaped behavior without authoring a harness task. Check its card (`plungeai_list_agents {agent_id: "one-agent"}`) for parameters. The single-agent doors forward only `persona`: `plungeai_execute_agent` and `POST /v1/agents/{id}/execute` drop `skills` and `experts` without an error. ## Platform skill packs (this one included) The `plungeai*` skill family you are reading is itself distributed through this system. What works today: the platform-internal variant is stored in skill-memory, so missions can declare `skills: [plungeai-workflows]` and get the CNL authoring rules injected — useful when a mission itself must write workflows. The family installs from the catalog at `https://skills.plungeai.com` (or `ocean-cli skills install`). ## Debugging checklist 1. Run ignored a skill → check the run's warnings for `not found` / deferral notes (`plungeai-results-traces` shows run detail). 2. Skill listed under AVAILABLE ON DEMAND but never loaded → the agent judged it irrelevant; make the mission text reference it explicitly, or move it earlier in the list. 3. Behavior must be guaranteed → put it in `instructions:` (always appended) rather than relying on a skill body surviving the budget. 4. Verify what a saved skill contains before trusting injection: run a quick mission whose goal is to `load_skill` it and echo the body. # plungeai-tools-connectors Source: https://www.plungeai.com/docs/skills/plungeai-tools-connectors Call PlungeAI structured tool-agents with typed parameters against a published invocation contract: fetch the contract (`plungeai_get_tool_contract` / `GET /v1/tools/{id}`), execute with typed `params` (`plungeai_execute_tool` / `POST /v1/tools/{id}/execute`), see which credential a tool runs on (the card's `credential` tier), connect an account over the API (`/v1/connections`, `on_behalf_of` for your customers) and pair chat channels (`/v1/channels`), and handle the trust fence (gated/money operations refuse unattended — `403 refused` on REST, `needs_approval`/`needs_connection` outcomes on MCP). Use when a card has a Parameters table (document conversion, weather, data-table CRUD, calendar, payments, posting), before the first call to an unfamiliar tool, or when fixing a `422 invalid_params` / `403 refused` / `424 connection_required` response. For prompt-driven agents use `plungeai-agents`; for finding a tool id or contract use `plungeai-discovery`; for the model catalog use `plungeai-models`. [Download zip](https://skills.plungeai.com/plungeai-tools-connectors.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-tools-connectors/SKILL.md) A **tool** is a structured agent: instead of free-text prompts it takes **typed parameters** against a published contract — named operations, a JSON Schema for inputs, worked examples, per-operation approval gates, and (over MCP) the acting user's live credential status. The rule that prevents almost every tool failure: **fetch the contract before the first call to an unfamiliar tool — the contract IS the API documentation.** ## Prerequisites - Self-service `ozk_` key from **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`). - MCP: `https://mcp.plungeai.com/v1`. REST: `https://api.plungeai.com`. ## Discovery first Find the tool (`plungeai_list_agents {search}` / `GET /v1/discovery/search`, see `plungeai-discovery`), then fetch its contract: ```json plungeai_get_tool_contract {agent_id: "markitdown"} ``` ```bash curl -s https://api.plungeai.com/v1/tools/markitdown -H "Authorization: Bearer ozk_YOUR_KEY" ``` Read it in order: `operations` (pick by `purpose`; note `gated: true`), `inputSchema` (exact param shapes), `examples` (worked CNL), then — MCP contract only — the **Credentials** section (see below). ## Execute — MCP ```json plungeai_execute_tool { agent_id: "markitdown", operation: "convert", params: { file_data: "<base64 of the file>", file_name: "report.pdf" } } ``` Answers a structured outcome envelope over HTTP 200, always — `ok` \| `needs_input` \| `needs_connection` \| `needs_api_key` \| `needs_approval` \| `unavailable` \| `error`. Full parameter table and the outcome table — [`references/mcp-execute-tool.md`](/skills/plungeai-tools-connectors/references/mcp-execute-tool). ## Execute — REST ```bash curl -s -X POST https://api.plungeai.com/v1/tools/brave-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"operation": "search", "params": {"query": "cloudflare workers pricing"}}' ``` Same fences, REST spelling: `403 refused` (fence — never retry), `404 unknown_tool`, `424 connection_required` (carries `connect`), `409 approval_required` (a dispatched run's own outcome paused for approval — approve out-of-band in Studio, then re-issue the identical request; REST has no continuation token like MCP's `plungeai_continue`), `422 invalid_params` (body carries `missing` + the full contract — self-correct and retry once). Full error catalogue, the 403 fence-vs-lifecycle distinction, streaming/format — [`references/rest-tools-plane.md`](/skills/plungeai-tools-connectors/references/rest-tools-plane) and [`references/contract-and-execution.md`](/skills/plungeai-tools-connectors/references/contract-and-execution). ## Connected accounts (credential status) Every card carries a `credential` tier (REST: `GET /v1/tools`, `GET /v1/tools/{id}`, `GET /v1/discovery/search`): `{"tier": "platform"}` runs on our keys, `{"tier": "connection", "provider", "method": "oauth" | "api_key" | "wallet"}` acts as the caller (or the caller's customer) and needs that provider connected first. Over MCP the contract's Credentials section is LIVE per acting user: - `🔐 platform-managed` — nothing to connect. - `✅ connected as <email>` — call away. - `⚠️ NOT connected` / `expired — reconnect` — execution answers `needs_connection`/`needs_api_key` (MCP) or `424 connection_required` / `credential_required` (REST) until the account is connected. Relay exactly what to connect; retry the identical call after. ## Over the One API — tools, connections, channels One `ozk_` key (`Authorization: Bearer`; setup: `plungeai-api-setup`). | Job | REST | MCP | CLI `ocean-cli …` | |---|---|---|---| | list tools | `GET /v1/tools?limit=&offset=` (≤ 100) → `{tools: [card + credential], count}` | `plungeai_list_agents` | `tools list` | | contract | `GET /v1/tools/{id}` → `{agent_id, operations, inputSchema, examples, credential, …}`; `404 unknown_tool` | `plungeai_get_tool_contract` | `tools get ID` | | execute | `POST /v1/tools/{id}/execute {operation?, params?, prompt?, format?}` → `200 {ok, content, outcome, execution_id, request_id}` | `plungeai_execute_tool` | `tools call ID [OP] --params JSON` | | connect | `POST /v1/connections/{provider} {api_key, api_secret?, on_behalf_of?}` → `201 {provider, on_behalf_of, status: "connected"}`; `POST /v1/connections/{provider}/link {on_behalf_of?, return_url?}` → `201 {url, expires_at}` | — | `connections add-key` (Studio session) | | status | `GET /v1/connections?on_behalf_of=` → `{connections: [{provider, on_behalf_of, method, status, connected_at}]}` (never a secret) | the contract's Credentials section | `connections list` (Studio session) | | disconnect | `DELETE /v1/connections/{provider}?on_behalf_of=` → `204`; `404 connection_not_found` | — | `connections remove` (Studio session) | | paired chats | `POST /v1/channels/pair {channel}` → a 10-minute code; `GET /v1/channels` → your paired chats | — | — | What builders get wrong: - **A `424` tells you where to connect.** `error.connect` is `{provider, method, link}`, and `link` is a route (`POST /v1/connections/<provider>/link`), not a URL: call it to mint the one-use, 15-minute hosted page. `oauth` providers connect through that page; `POST /v1/connections/{provider}` takes a key and is checked live (`400 credential_rejected`; `404 unknown_provider`). - **`on_behalf_of` is for your customers and lives on `POST /v1/execute` only.** The per-id routes (`/v1/tools/{id}/execute`, `/v1/agents/{id}/execute`, `/v1/workflows/{id}/execute`) answer `400 invalid_body` rather than quietly run as you. Read such a run back with `GET /v1/executions/{id}/output?on_behalf_of=`. - **Operation fields can sit bare at the body root** (`POST /v1/tools/calculator-tool-agent/execute` with `{"expression": "7*6"}`); `params` still works and wins on a clash. - Connection routes and `/v1/channels` are tenant-scoped like everything else: another account's ids answer the same as missing ones. A paired chat is then bound to a bot with `POST /v1/workflows/{id}/bindings` (`plungeai-api-setup`, `references/bots-and-door.md`). ## Why the fences exist Gated operations are the ones with real-world blast radius: money movement, outbound messages, irreversible mutations. **An unattended caller never fires them** — a human must be in the loop. `403`/`409`/`needs_approval` are correct behavior, not errors to engineer around: surface them, get the human decision, continue through the approval mechanism (`plungeai_continue` on MCP). A retried tool call re-fires the FULL operation — check execution history before re-firing anything non-idempotent. ## Gotchas - **Reserved param names** (`agent`, `type`, `id`, `operation`, `depth`, `user_id`, `executor_user_id`, `workflow`, `execution_id`) collide with the task envelope and are never forwardable — `needs_input`/`422` names them. - **A lone `prompt` on a multi-operation card runs the DEFAULT operation** and warns about it — if that's not what the user meant, re-call with an explicit `operation` + typed `params`. - **`gated: true` is refused regardless of operation wording** — rephrasing the prompt or switching styles to sneak past a fence never works. - **`409 approval_required` IS real on REST** — it fires when a dispatched run's own outcome comes back `needs_approval`, not from the pre-dispatch gated-verb/guarded-category fence (that fence runs in unattended mode on this route and always resolves to `403 refused` instead). Approve out-of-band, then re-issue the identical request. ## Related skills - `plungeai-discovery` — search, full cards, the contract fetch itself. - `plungeai-agents` — prompt-driven execution, `plungeai_get_result`. - `plungeai-models` — model/provider overrides, the money plane. - `plungeai-workflows` — put a tool's typed fields directly on a CNL task. - `plungeai-results-traces` — tracing an `execution_failed`/`upstream_error`. ## Reference pages <CardGroup cols={2}> <Card title="Tools — structured tool-agents and their contracts" icon="file-text" href="/skills/plungeai-tools-connectors/references/contract-and-execution"> A tool on PlungeAI is a structured agent: instead of free-text prompts it takes typed parameters against a published contract — named operations, a JSON… </Card> <Card title="Executing tools over MCP — execute_tool" icon="file-text" href="/skills/plungeai-tools-connectors/references/mcp-execute-tool"> Structured tool-agents (cards with a Parameters table — posting, document conversion, weather, data-table CRUD, calendar operations, payment actions) take… </Card> <Card title="Tools plane — /v1/tools" icon="file-text" href="/skills/plungeai-tools-connectors/references/rest-tools-plane"> Every active agent is also callable as a tool with a machine-checkable invocation contract. </Card> </CardGroup> # Tools — structured tool-agents and their contracts Source: https://www.plungeai.com/docs/skills/plungeai-tools-connectors/references/contract-and-execution <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/mcp-gateway/server.ts, orchestration/mcp-gateway/tools.ts, core/core-base/tool-contract.ts, core/core-base/call-agent.ts | last-synced: 2026-10-05 (credential tier, 424 connect pointer, execution_id on the 200; earlier re-verified: reserved-param list against tool-contract.ts RESERVED_TASK_FIELDS, 409/403 fence behavior against call-agent.ts checkAgentCall unattended vs interactive branches, and routes/tools.ts execute handler — all match, no drift found) --> A **tool** on PlungeAI is a structured agent: instead of free-text prompts it takes **typed parameters** against a published contract — named operations, a JSON Schema for inputs, worked examples, and per-operation approval gates. Think of document converters, weather lookups, data-table CRUD, calendar operations, payment actions. The rule that prevents almost every tool failure: **fetch the contract before the first call to an unfamiliar tool. The contract IS the API documentation** — current, generated from the live card, and (over MCP) personalized with the acting user's credential status. ## The contract `GET /v1/tools/{id}` (One API) or `plungeai_get_tool_contract {agent_id}` (MCP): ```json { "agent_id": "markitdown", "name": "markitdown", "description": "Convert a document, image, audio file, or URL to markdown…", "operations": [ { "name": "convert", "purpose": "Convert a file or URL to markdown", "gated": false, "required_params": ["file_data", "file_name"] } ], "inputSchema": { "type": "object", "properties": { "…": {} } }, "examples": [ { "title": "Convert a PDF to markdown", "cnl": "…worked YAML…" } ], "output_type": "markdown", "credential": { "tier": "platform" } } ``` Read it in this order: 1. **`operations`** — pick the operation whose `purpose` matches the job. Note `gated: true`: that operation needs human approval (see fences below). 2. **`inputSchema`** — the exact parameter shapes. `required_params` per operation is the short list; the schema is the authority on types and enums. 3. **`examples`** — worked CNL YAML; each example also shows how the tool is used as a workflow task (the same fields go on a task next to `agent:` — see the `plungeai-workflows` skill). 4. **Credential** — the REST and MCP contract carry the `credential` tier: `{"tier": "platform"}` just works; `{"tier": "connection", "provider", "method"}` needs that provider connected first (`POST /v1/connections/{provider}`, or in Studio). The MCP contract adds the LIVE per-user status: "platform-managed" means it just works; "connect Google first" means the call returns `needs_connection` until the user connects it. ## Discovery ```bash # List active tool-agents curl -s "https://api.plungeai.com/v1/tools?limit=50" \ -H "Authorization: Bearer ozk_YOUR_KEY" # → {tools: [Card…], count} ``` Or find by capability with the hybrid search (`GET /v1/discovery/search?q=…`) / `plungeai_list_agents {search}` — tools are agents; they appear in the same registry with a Parameters table on their card. ## Execution ### MCP: `plungeai_execute_tool` ``` plungeai_execute_tool { agent_id: "markitdown", operation: "convert", params: { file_data: "<base64 of the file>", file_name: "report.pdf" } } ``` Runs through the engine with full observability (an execution id you can trace). Long jobs: `mode: "async"` → poll `plungeai_get_workflow_status` → fetch with `plungeai_get_result`. Every call answers with a **structured outcome envelope over HTTP 200** — a blocked call is guidance, never a protocol error. The complete MCP status vocabulary: | Outcome | Meaning | Your move | |---|---|---| | `ok` | Result attached | Relay it verbatim | | `needs_input` | Missing/invalid fields; the outcome carries them + the schema | Fix exactly those fields; retry once | | `needs_connection` / `needs_api_key` | User credential missing | Tell the user exactly what to connect in the platform apps; retry after | | `needs_approval` | A gated/money operation paused the run (`⏸`) | Relay the approval block verbatim; only after the user explicitly approves, `plungeai_continue {approve: true}`; their "no"/changes go in `message` | | `unavailable` | Tool/dependency not live (or fenced off); alternatives listed | Pick an alternative or re-discover | | `error` | Downstream execution failed; the summary names the cause | Read it; inspect the trace (`plungeai-results-traces`); do not hammer-retry | There is no `refused` MCP status: MCP has an approval surface, so gated operations pause as `needs_approval` instead of being refused. `refused` is the One API's REST spelling of the fence (`403` below). ### One API: `POST /v1/tools/{id}/execute` ```bash curl -s -X POST https://api.plungeai.com/v1/tools/markitdown/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{ "operation": "convert", "params": { "file_data": "<base64 of the file>", "file_name": "report.pdf" } }' # 200 → {"ok": true, "content": "…", "outcome": "ok", "execution_id": "…", "request_id": "…"} ``` `prompt` is accepted instead of `operation`/`params` for prompt-driven agents reached through this route — but if the card has a Parameters table, use typed params. HTTP status mapping (same fences, REST spelling): | Status | Code | Meaning | |---|---|---| | `403` | `refused` | Fence — gated/money operation refused unattended. Do not retry. | | `404` | `unknown_tool` | Id not in the live catalog — re-discover | | `409` | `approval_required` | The dispatched run's own outcome paused for approval (`needs_approval`) — real and reachable on this route. Approve out-of-band (Studio), then re-issue the identical request; the One API has no continuation token like MCP's `plungeai_continue`. | | `422` | `invalid_params` | Body carries the missing/reserved fields AND the contract — fix exactly those | | `424` | `connection_required` / `credential_required` | No connected account or key for a connection-tier agent; `error.connect` names the provider and the route that connects it | | `502` | `execution_failed` | Downstream failure — inspect the trace (`plungeai-results-traces`) | ## Why the fences exist Gated operations are the ones with real-world blast radius: money movement, outbound messages, irreversible mutations. The platform's trust model is that **an unattended caller never fires them** — a human must be in the loop. `403`/`409` are therefore correct behavior, not errors to engineer around. Surface them, get the human decision, continue through the approval mechanism. Also remember: a retried tool call re-fires the FULL operation — if the side effect completed before the failure was reported, a retry duplicates it. When in doubt, check execution history (`plungeai_executions`) before re-firing anything non-idempotent. ## Tools inside workflows A structured tool-agent is used in CNL by putting its typed fields directly on the task, next to `agent:`: ```yaml - type: task id: convert agent: markitdown operation: convert file_data: "{input}" file_name: report.pdf ``` The contract's `examples[].cnl` show the canonical per-tool shapes. Everything else about composition is in the `plungeai-workflows` skill. ## Checklist 1. Contract fetched (first use) — operation picked by `purpose`, gates noted. 2. Params match `inputSchema` exactly — no extra prose fields. 3. Credential status green (MCP) or the user warned. 4. Gated op → plan for the approval pause; unattended REST cannot serve it. 5. Outcome remediation followed; no blind retries; no duplicate side effects. # Executing tools over MCP — execute_tool Source: https://www.plungeai.com/docs/skills/plungeai-tools-connectors/references/mcp-execute-tool <!-- sources-of-truth: orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/server.ts, orchestration/mcp-gateway/tool-exec.ts, orchestration/mcp-gateway/registry-cards.ts, orchestration/mcp-gateway/tool-outcome.ts, orchestration/mcp-gateway/credential-preflight.ts | last-synced: 2026-09-24 (execute_tool half of the original discovery-and-execution.md; the list_agents/get_tool_contract half lives in plungeai-discovery, the execute_agent/get_result half lives in plungeai-agents. Re-verified ExecuteToolSchema + reserved-param list against tool-exec.ts/tool-contract.ts — all match, no drift found) --> Structured tool-agents (cards with a Parameters table — posting, document conversion, weather, data-table CRUD, calendar operations, payment actions) take typed fields against a published contract, not prose. Always fetch the contract first (`plungeai_get_tool_contract`, see `plungeai-discovery`) — the contract IS the API documentation, generated live from the card and personalized with the acting user's credential status. ## plungeai_execute_tool **Purpose:** run one structured tool-agent directly with typed params, through the same engine pipeline as every other execution (history, results, HITL all included). **Parameters** | Param | Type | Notes | |---|---|---| | `agent_id` | string, required | From a live search (`plungeai_list_agents`). | | `operation` | string ≤128 | One of the contract's operations. Also auto-filled into the card's selector param (`action`/`op`/`operationType`) when the card reads its operation from a param — so passing `operation` alone is safe. | | `params` | object | Typed fields per the contract's JSON Schema. | | `prompt` | string | Prose fallback. On a card whose requireds resolve to exactly ONE free-text param, a lone prompt is mapped into it (visibly, with a warning). Never mapped into selectors, enums, or URL params. | | `mode` | `sync` \| `async` | Async returns an execution id to poll. | | `format` | `markdown` \| `json` | Response serialization; default markdown. | **Example** ```json {"user_request": "what's the weather in Lisbon this weekend?", "agent_id": "weather-agent", "operation": "forecast", "params": {"location": "Lisbon", "days": 3}} ``` **Returns:** the outcome envelope, always (never a raw error): | Status | Meaning → your move | |---|---| | `ok` | Result markdown + execution-id footer (+ any ⚠️ warnings, e.g. "prose mapped"). Relay verbatim. | | `needs_input` | Missing/invalid fields; `missing[]` + the full contract ride along so you can fix the SAME call without a second lookup. Nothing was executed. | | `needs_connection` / `needs_api_key` | The USER must connect a service or save a key in a PlungeAI app. Relay the instructions, wait for them, retry the identical call. Caught pre-flight when possible — before a run exists. | | `needs_approval` | ⏸ gated operation paused mid-run. Relay verbatim; `plungeai_continue` after the user decides. | | `unavailable` | Agent refused (unknown/inactive) or temporarily down — real alternatives included. Nothing was executed. | | `error` | Terminal failure with a reason; follow the remediation (often `retry_with {mode: "async"}` for timeouts). | **Failures & fixes** - Reserved param names (colliding with the task envelope) → `needs_input` naming them; rename/drop those keys. The reserved names: `agent`, `type`, `id`, `operation`, `depth`, `user_id`, `executor_user_id`, `workflow`, `execution_id`. - Prose prompt + no `operation` on a multi-operation card → the run executes the card's DEFAULT operation and says so in a warning. If that is not what the user meant, re-call with an explicit `operation` + typed params. - Gated operations are not refused up front — they pause (`needs_approval`). That pause is a trust fence: surface it, never bypass it. ## Connected accounts (credential status) The contract's Credentials section (fetched via `plungeai_get_tool_contract`, see `plungeai-discovery`) is LIVE per acting user: - `🔐 platform-managed` — nothing to connect; just call. - `✅ connected as <email>` — the user's OAuth/API-key connection is live; call away. - `⚠️ NOT connected` / `expired — reconnect` — `plungeai_execute_tool` will answer `needs_connection` / `needs_api_key` until the user connects that credential in a PlungeAI app (Studio → Connectors). Relay exactly what to connect; do not retry until they have. The credential-status RPC runs speculatively in parallel with the card fetch on `plungeai_get_tool_contract`, so the contract call itself carries this status — no separate lookup needed before your first `execute_tool` attempt. ## Putting it together — the canonical multi-step run ``` 1. plungeai_list_agents {search: "convert pdf to markdown"} → markitdown 2. plungeai_get_tool_contract {agent_id: "markitdown"} → schema; credentials 🔐 platform-managed 3. plungeai_execute_tool {agent_id: "markitdown", operation: "convert", params: {file_data: "<base64 of the file>", file_name: "report.pdf", format: "markdown"}, mode: "async"} → execution_id E 4. plungeai_get_workflow_status {execution_id: E} → running → completed 5. plungeai_get_result {execution_id: E} → relay verbatim (see plungeai-agents) ``` Note how step 3 follows step 2's contract: `convert` requires `file_data` (base64-encoded content) and `file_name` for uploaded files (a `url` param is also accepted for web pages/YouTube/Wikipedia, but a file already in hand goes through `file_data`). Skip step 2 only for agents whose contract you fetched earlier in the same conversation. Skip steps 4–5 in sync mode — the result comes back in the execute call itself. # Tools plane — /v1/tools Source: https://www.plungeai.com/docs/skills/plungeai-tools-connectors/references/rest-tools-plane <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/tools.ts, orchestration/api-gateway/routes/connections.ts, core/core-base/credential-tiers.ts, orchestration/api-gateway/docs/guide-3.0/06-tools.md, orchestration/api-gateway/docs/guide-3.0/07-connectors.md | last-synced: 2026-10-05 (One API 2.3.0: credential tier on cards, bare root fields, 424 connect pointer, /v1/connections; earlier re-verified against orchestration/api-gateway/routes/tools.ts, _util.ts, core/core-base/call-agent.ts; fixed a stale "Settings → Integrations / Connectors" UI label to the real one, Studio → Connectors, per apps/ocean-studio/src/components/connectors/) --> Every active agent is also callable as a **tool** with a machine-checkable invocation contract. The flow is always: **discover → inspect the contract → execute**. This plane runs the platform's trust fence — gated and money operations refuse unattended execution by design. Auth: `Authorization: Bearer ozk_YOUR_KEY` on every route here. ## GET /v1/tools — list active tool-agents Query params: `limit` (default 50, max 100), `offset` (default 0). ```bash curl -s "https://api.plungeai.com/v1/tools?limit=2" \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "tools": [ { "id": "brave-agent", "name": "Brave Search", "type": "agent", "category": "search", "status": "active", "description": "Web search via Brave", "tags": ["search"], "credential": { "tier": "platform" } }, { "id": "pdf-agent", "name": "PDF Agent", "type": "agent", "category": "documents", "status": "active", "description": "Convert and extract PDFs", "tags": ["pdf"], "credential": { "tier": "platform" } } ], "count": 2 } ``` `count` is the returned page's length, not the catalog total — page until a short page, never until `offset >= count`. A failed registry hop returns `502 upstream_error`. ## GET /v1/tools/{id} — the invocation contract Fetch this **before the first execute** of any unfamiliar tool. The contract tells you the operations, which are gated, the required params, the JSON Schema for inputs, and worked examples. ```bash curl -s https://api.plungeai.com/v1/tools/brave-agent \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "agent_id": "brave-agent", "name": "Brave Search", "description": "Web search via the Brave Search API", "operations": [ { "name": "search", "purpose": "Run a web search", "gated": false, "required_params": ["query"] }, { "name": "news", "purpose": "Search recent news", "gated": false, "required_params": ["query"] } ], "inputSchema": { "type": "object", "properties": { "query": { "type": "string" }, "count": { "type": "integer" } } }, "examples": [ { "title": "Basic web search", "cnl": "operation: search\nparams:\n query: cloudflare workers" } ], "output_type": "markdown", "credential": { "tier": "platform" } } ``` 404 → `{"error":{"code":"unknown_tool","message":"..."}}` — the id isn't an active tool. Re-discover via `GET /v1/tools` or discovery search. `gated: true` on an operation means executing it unattended is refused with **403 `refused`** by this route's pre-dispatch fence (`checkAgentCall` runs in `'unattended'` mode here, which always resolves a gated-verb/guarded- category hit to a refusal, never a pause). `409 approval_required` is a *different*, real code path on this same route — see the fence section below. ## POST /v1/tools/{id}/execute — execute an operation Body — three styles, all valid (JSON, or `text/yaml` in and out): | Field | Type | Notes | |---|---|---| | `operation` | string | Operation name from the contract | | `params` | object | Params per the contract's `inputSchema` | | `prompt` | string | Free-text alternative for prompt-driven tools | | *(bare fields)* | any | Operation params may also sit at the body root (`{"expression": "7*6"}`); `params` wins on a name clash | | `format` | string | Response negotiation: `json` (default) \| `yaml` \| `markdown` \| `text` — same values as an `Accept` header or the request `Content-Type` mirror | Validation rules ("reserved" = param names colliding with engine task-envelope fields — `agent`, `type`, `id`, `operation`, `depth`, `user_id`, `executor_user_id`, `workflow`, `execution_id` — never forwardable): - Reserved param names → 422, always. - Missing required params → 422 **only when no `prompt` is present** — a prose prompt satisfies prompt-capable agents, so a prompt-style call skips param validation entirely. The trust fence still runs either way. Structured style (preferred when the contract defines operations): ```bash curl -s -X POST https://api.plungeai.com/v1/tools/brave-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"operation": "search", "params": {"query": "cloudflare workers pricing"}}' ``` Prompt style: ```bash curl -s -X POST https://api.plungeai.com/v1/tools/brave-agent/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "search: cloudflare workers pricing"}' ``` Success — HTTP 200: ```json { "ok": true, "content": "# Web Search: cloudflare workers pricing\n\n1. ...", "outcome": "success", "execution_id": "00000000-0000-4000-8000-000000000003", "request_id": "00000000-0000-4000-8000-000000000004" } ``` ## Error semantics — features, not failures | Status | Code | Meaning | Correct handling | |---|---|---|---| | 400 | `invalid_format` | `format` isn't one of `json`/`yaml`/`markdown`/`text` | Fix the field | | 404 | `unknown_tool` | No such active tool | Re-discover the id | | 422 | `invalid_params` | Contract violation — response body carries `missing`, `reserved`, `warnings`, **and the full contract** | Self-correct from the echoed contract; retry once with fixed params | | 403 | `refused` (fence) | Gated operation, or payment/blockchain-category agent — never callable unattended | **FINAL. Surface verbatim. NEVER retry, rephrase, or work around** | | 403 | `refused` (lifecycle) | Parked agent (`status:developing/development/developed`) or unknown card — the `reason` says which | Re-discover via `GET /v1/tools` / discovery search | | 409 | `approval_required` | The dispatched run's own outcome came back `needs_approval` (mapped 1:1 via the outcome→HTTP table — guide 3.1 §13.3/§13.5). NOT produced by the pre-dispatch gated-verb/guarded-category fence, which always resolves to 403 on this route (see below) | Approve out-of-band (Studio / an attended surface), then re-issue the identical request | | 409 | `duplicate_execution_id` | The `x-trace-id` header was already used by an earlier run | Send a fresh UUID | | 413 | `payload_too_large` | Body over 1 MiB (`MAX_REQUEST_SIZE`) | Shrink the payload (e.g. stream large files through a URL param instead of inline `file_data`) | | 424 | `connection_required` / `credential_required` | The agent needs a connection or key the acting user hasn't set up; `error.connect` is `{provider, method, link}` | Connect it over the API (`POST /v1/connections/{provider}` with a key, or `POST /v1/connections/{provider}/link` for the hosted page) or in Studio → Connectors; retry the identical call | | 429 | `rate_limited` | Per-key limiter — `Retry-After` header | Back off and retry | | 502 | `execution_failed` | The dispatched agent failed — body carries `outcome`, `detail`, `content` | Act on `outcome` (`needs_input`, `needs_connection`, …) before blind-retrying; one retry only for transient outcomes | | 502 | `upstream_error` | Registry/engine hop threw (any route in this plane) | One retry with backoff | | 503 | `agent_unavailable` | Agent temporarily down | Retry later or pick an alternative | ### 422 example — the contract comes back to you ```json { "error": { "code": "invalid_params", "message": "params failed card validation", "missing": ["query"], "reserved": [], "warnings": [], "contract": { "agent_id": "brave-agent", "operations": [ "..." ] } } } ``` Generated code should parse `missing` and the embedded `contract`, repair the body, and retry exactly once. If the second attempt also 422s, stop and report. ### 403 — the trust fence, and how to tell refusals apart **Fence refusals are FINAL.** The gated-verb floor (`buy`, `purchase`, `fetch_paid`, `send`, `transfer`, `pay`, `send_payment`, `withdraw`, `shop`), operations the card tags `gated:`, and payment/blockchain-category agents (refused **regardless of operation** on this surface): ```json { "error": { "code": "refused", "message": "operation \"transfer\" on x is gated (outward/irreversible) and requires human approval — not available in unattended runs." } } ``` Do not loop, do not reword the prompt to sneak past the fence, do not switch to the prompt style to avoid the operation gate. Tell the user what was refused and why; approvals happen on attended surfaces (Ocean Studio / apps), not through this API. **Lifecycle refusals are not final** — the same 403 `refused` code also covers parked agents ("parked (status:…) — not callable. Use registry search to find an active alternative.") and ids with no registry card. The `reason` string distinguishes them; the remedy is re-discovery, not surrender. **`409 approval_required` is real and reachable on this route** — but not from the check above. `POST /v1/tools/{id}/execute` calls the shared gated-verb/guarded-category fence (`checkAgentCall`, `core/core-base/ call-agent.ts`) in `'unattended'` mode, and in that mode every branch that could otherwise pause for approval (a guarded payment/blockchain category with no recognizable operation, or a gated verb) resolves to `403 refused` instead — `approval_required` is only returned by that function in `'interactive'` mode, which this route never uses. The 409 you actually see on this plane comes from a **different** source: the dispatched run's own outcome. If the downstream agent/tool run itself pauses mid-execution (outcome `needs_approval` — the same status MCP surfaces as a ⏸ block), the outcome→HTTP mapping (guide 3.1 §13.3, "Trust fence" §13.5) turns that into `409 approval_required` here too. Handling: **approve out-of-band** (Studio or another attended surface), **then re-issue the identical request** — the One API has no continuation token like MCP's `plungeai_continue`, so you resend rather than resume. ## Tools vs agents — which plane to call - `/v1/tools/:id/execute` — structured, contract-validated, fenced. Use when you know the operation and params (typical for generated app code). - `/v1/agents/:id/execute` — free prompt, sync/async semantics, no operation contract. Use for open-ended instructions to LLM-driven agents. Same underlying agents, different invocation disciplines. ## Connections — `/v1/connections` and `/v1/channels` A connection-tier agent runs on the credential stored here. All four routes take the same `ozk_` key, answer `503 connections_unavailable` where the store is not bound, and never return a secret. | Route | Body / query | Answer | |---|---|---| | `POST /v1/connections/{provider}` | `{api_key, api_secret?, on_behalf_of?}` | `201 {provider, on_behalf_of, status: "connected"}`; the key is probed live where the provider has a probe (`400 credential_rejected`, nothing stored); `400 missing_api_key`; `404 unknown_provider` | | `POST /v1/connections/{provider}/link` | `{on_behalf_of?, return_url?}` (https) | `201 {url, expires_at}`: a signed Studio page, single-use, 15 minutes, no Studio login — send it to the person who owns the account | | `GET /v1/connections` | `?on_behalf_of=` | `200 {connections: [{provider, on_behalf_of, method, status, connected_at}]}` | | `DELETE /v1/connections/{provider}` | `?on_behalf_of=` | `204`; `404 connection_not_found` | `provider` is the connector id the card's `credential.provider` or a 424's `connect.provider` names (`google`, `azure` for Microsoft 365, `github`, `slack`, `stripe`, …). `on_behalf_of` is your customer's id (1–128 characters of `A-Z a-z 0-9 _ . -`, no `..`): the connection is theirs, stored under your key, reachable only through it; an empty value is `400 invalid_on_behalf_of`, never read as your own account. To run a tool agent as that customer call `POST /v1/execute` with `on_behalf_of` — the per-id execute routes refuse it (`400 invalid_body`). `POST /v1/channels/pair {channel}` (`telegram`, `whatsapp`, `discord`, `teams`) returns a code good for 10 minutes; send it to the channel's bot from the chat, and the chat appears in `GET /v1/channels` with the `sender_id` that `POST /v1/workflows/{id}/bindings` takes. `503 not_enabled` where the message gateway is not configured. # plungeai-workflows Source: https://www.plungeai.com/docs/skills/plungeai-workflows Build, validate, test, and save PlungeAI (Ocean Studio) workflows in CNL YAML. Use when the user wants to create or edit a PlungeAI/Ocean workflow, orchestrate PlungeAI agents, write CNL YAML, run a multi-agent pipeline on PlungeAI, or turn research in this chat into a saved Studio workflow. Triggers: "PlungeAI workflow", "Ocean Studio workflow", "CNL", "build a workflow", "save this as a workflow", "plungeai agents". [Download zip](https://skills.plungeai.com/plungeai-workflows.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-workflows/SKILL.md) Author CNL workflow YAML for the PlungeAI platform, prove it works, and save it so it appears in Ocean Studio and every connected app. ## Prerequisites (check first, once) - Preferred: the `plungeai` MCP server is connected (tools named `plungeai_*` are available). Confirm identity with `plungeai_whoami`. - Otherwise: you can still author correct YAML and hand the user a paste-ready block (see [`references/channels.md`](/skills/plungeai-workflows/references/channels), Channels 2-3). ## The loop 1. **Understand the goal.** One sentence: input → steps → deliverable. Ask only if the goal is genuinely ambiguous. 2. **Discover agents — never from memory.** `plungeai_list_agents {search: "<capability in plain words>"}` per capability. Take ids verbatim from results (kebab-case). Unknown or inactive ids are refused at execution. Fetch a full card with `{agent_id}` before using an unfamiliar agent — cards carry parameters and "Not for → use X instead" redirects. 3. **Author the YAML.** Load [`references/cnl-spec.md`](/skills/plungeai-workflows/references/cnl-spec) and pick a shape from [`references/recipes.md`](/skills/plungeai-workflows/references/recipes). Independent steps go in `parallel`; chained steps in `sequential` with `{data:task_id}`; goal-driven open-ended work is ONE `type: harness` task, not many small tasks. Long prompts use block scalars (`prompt: |`) — never hard-wrap a value. 4. **Test ad-hoc BEFORE saving.** `plungeai_execute_workflow {workflow_yaml, input}` (`mode: "async"` + `plungeai_get_workflow_status` for long runs). Read the actual output — does it satisfy the user's goal? Iterate the YAML, not the save. 5. **Save.** `plungeai_workflow {action: "create", name, yaml, description}`. The platform re-validates server-side; a refusal lists field errors — fix exactly those and retry. On success it syncs live to Studio and peer apps. `kind` files it as `workflow` (default), `agent`, `bot` or `campaign`; a bot or agent is exactly ONE `type: harness` task on `agent: one-agent` (author it with `plungeai-bot-agent`). Iterations on a saved workflow: `action: "update"` (plus `"save_version"` before big changes). 6. **Learn-back (offer it).** If real research happened in this chat, offer: `plungeai_learn {source: "<distilled findings>"}` to save it as a reusable platform skill that future runs can inject. ## Hard rules (the top causes of refused YAML) - Agent ids ONLY from a live `plungeai_list_agents` search. `brave-agent`, never `brave-search`. - These do NOT exist: `$variable`, `depends_on`, `outputs:`, `parallel: true`, `schedule:` (a schedule is a scheduler job — Studio, `plungeai_schedule`, `/v1/schedules` — never YAML). - `parallel` / `sequential` / `batch` / `dynamic` / `debate` / `validate` blocks never carry `agent:` — agents go on the inner tasks. - `type: task` reads `prompt:` (or `query:`) — not input/instructions/message. `goal:` belongs to `type: harness` only. - Every `id` unique; data flows automatically — never hand-wire results. - Search agents (brave-agent, tavily-agent, …) take SEARCH TERMS — short queries, not instructions. Synthesis, analysis, and "based on the research above…" prompts belong on `one-agent`. ## Saved bots — policy, files, chats, groups (every door) A saved `kind: bot` carries a stored policy (permission presets incl. `hand_off`, admin locks, `budget_usd_run` / `budget_usd_month`) that the engine applies on EVERY door. Keys and meaning: `plungeai-bot-agent`. The control verbs: | Job | MCP `plungeai_workflow` action | REST (`/v1/workflows/{id}/…`) | CLI `ocean-cli workflows …` | |---|---|---|---| | policy | `permissions_get`, `permissions_set` | `GET`/`PUT …/permissions` | `permissions get\|set --class pay=ask --lock pay --unlock pay --budget-run 0.5 --budget-month 20` | | files | `files`, `file` (inline base64 ≤ 5 MB) | `GET …/workspace-files`, `…/download?path=` | `files <id> [--download <path> --out <file>]` | | inbox | `inbox` | `GET …/inbox` | `inbox <id>` | | chat bindings | `bindings`, `bind`, `unbind` | `GET`/`POST …/bindings`, `DELETE …/bindings/{channel}/{sender_id}` | `bindings list\|bind --channel telegram --sender <id>\|unbind --channel --sender --yes` | | groups | `groups`, `group_set`, `group_delete`, `group_thread` | `/v1/bot-groups`, `/{id}`, `/{id}/thread` | `ocean-cli bot-groups list\|create --name --members a,b\|update\|delete\|thread <id>` | | versions | `save_version`, `list_versions`, `get_version`, `restore_version` | `GET`/`POST …/versions`, `POST …/versions/{versionId}/restore` | `save-version [--label]`, `versions`, `restore` | Setting `locks` needs a team admin (REST `403 lock_admin_required`; nothing in that body is written). Run steering and files live in `plungeai-results-traces`; schedules and triggers in `plungeai-scheduling`; all REST bodies in `plungeai-api-setup` (`references/bots-and-door.md`). ## When something fails - **create refused** → the message names field + problem; fix exactly that, retry once. - **"agent not active/unknown"** → re-search the registry; the catalog is live. - **execution succeeded but output is wrong** → improve prompts/structure and re-run ad-hoc; only save after the output is right. - **no MCP tools available** → Channels 2-3 in [`references/channels.md`](/skills/plungeai-workflows/references/channels). ## References (load on demand) - [`references/cnl-spec.md`](/skills/plungeai-workflows/references/cnl-spec) — full CNL v6 reference (all 10 task types, validation errors) - [`references/recipes.md`](/skills/plungeai-workflows/references/recipes) — canonical patterns + when to use each - [`references/channels.md`](/skills/plungeai-workflows/references/channels) — MCP (`https://mcp.plungeai.com/v1`) / Studio paste / raw HTTP API - `examples/*.yaml` — six validated, runnable workflows ## More - [`references/overview.md`](/skills/plungeai-workflows/references/overview) — the platform-level view: parallelism facts (measured, not guessed), placeholders and date tokens, execution/results/ follow-up semantics, failure semantics. - [`references/api.md`](/skills/plungeai-workflows/references/api) — the One API `/v1/workflows/*` HTTP surface: inline vs saved execution, SSE streaming events, result redemption, cancel, trace correlation, legacy aliases. - [`references/mcp.md`](/skills/plungeai-workflows/references/mcp) — the full MCP tool set for workflows (incl. the bot verbs above): `plungeai_execute_workflow`, `plungeai_get_workflow_status`, `plungeai_list_workflows`, `plungeai_workflow` (CRUD + versioning), `plungeai_build_workflow`, `plungeai_executions`, and `plungeai_templates` (`action: "use"` — instantiating a gallery template as a workflow; browsing is `plungeai-discovery`'s). ## Reference pages <CardGroup cols={2}> <Card title="Workflows plane — /v1/workflows" icon="file-text" href="/skills/plungeai-workflows/references/api"> Run multi-agent CNL workflows: inline (send the workflow document in the request), saved (reference a workflow id saved in Ocean Studio), each with an… </Card> <Card title="Getting a workflow into PlungeAI — three channels" icon="file-text" href="/skills/plungeai-workflows/references/channels"> Connect once (self-service ozk key from Dashboard → One API → Keys, `https://dashboard.plungeai.com`): </Card> <Card title="CNL Workflow Reference (engine v6) — PlungeAI" icon="file-text" href="/skills/plungeai-workflows/references/cnl-spec"> CNL (Cognitive Natural Language) is PlungeAI's YAML workflow language: you declare tasks and how they compose (parallel, sequential, batch, debate, harness… </Card> <Card title="Workflows — execute_workflow, get_workflow_status, list_workflows, workflow, build_workflow, executions" icon="file-text" href="/skills/plungeai-workflows/references/mcp"> Workflows are CNL YAML run by the platform engine. </Card> <Card title="Workflows — CNL orchestration: what it is and how to run it" icon="file-text" href="/skills/plungeai-workflows/references/overview"> A workflow is a YAML document (CNL — Cognitive Natural Language) that the engine executes as a DAG of agent calls: parallel fan-out, sequential pipelines… </Card> <Card title="CNL recipes — pick the shape, then adapt" icon="file-text" href="/skills/plungeai-workflows/references/recipes"> Full runnable versions live in examples/. Two composition rules apply to all of them: </Card> </CardGroup> ## Examples <AccordionGroup> <Accordion title="01-simple-search.yaml"> ```yaml 01-simple-search.yaml name: "Simple Single Task" version: "2.0.0" description: "Single search task — the smallest valid CNL workflow." tasks: - type: "task" id: "single_task" agent: brave-agent prompt: "simple search test" input: "electric vehicles 2026" ``` </Accordion> <Accordion title="02-parallel-research.yaml"> ```yaml 02-parallel-research.yaml name: "Parallel Research With Synthesis" version: "2.0.0" description: "Three parallel searches from different engines, then LLM synthesis. The most common production pattern." tasks: - type: "parallel" id: "research_phase" subtasks: - type: "task" id: "brave_search" agent: brave-agent prompt: "{input} — latest news, trends, and analysis" - type: "task" id: "tavily_research" agent: tavily-agent prompt: "{input} — cited sources and detailed reporting from the last 12 months" - type: "task" id: "exa_deep" agent: exa-agent operationType: "search" prompt: "{input} — academic papers, institutional research, and expert analyses" - type: "task" id: "synthesis" agent: one-agent prompt: | Synthesize the parallel research into a comprehensive report on {input}. Sources: Brave: {data:brave_search} Tavily: {data:tavily_research} Exa: {data:exa_deep} Identify common themes, contradictions, and the three strongest insights. input: "quantum computing commercial applications" ``` </Accordion> <Accordion title="03-hierarchical.yaml"> ```yaml 03-hierarchical.yaml name: "3-Level Hierarchical Nesting" version: "2.0.0" description: "parallel → sequential → parallel nesting. Demonstrates unlimited nesting depth with data flowing automatically." tasks: - type: "parallel" id: "level_1_parallel" subtasks: - type: "task" id: "simple_task" agent: brave-agent prompt: "{input} — quick overview" - type: "sequential" id: "level_2_sequential" subtasks: - type: "task" id: "deep_search" agent: tavily-agent prompt: "{input} — deep research with citations" - type: "parallel" id: "level_3_parallel" subtasks: - type: "task" id: "business_angle" agent: one-agent prompt: "From the research, extract business implications for {input}" - type: "task" id: "technical_angle" agent: one-agent prompt: "From the research, extract technical implications for {input}" - type: "task" id: "final_report" agent: one-agent prompt: "Combine all prior branches into a comprehensive report on {input}." input: "autonomous vehicle regulation" ``` </Accordion> <Accordion title="04-validate-consensus.yaml"> ```yaml 04-validate-consensus.yaml workflow: name: "Consensus Validation With 3 Validators" version: "2.0.0" description: "Three LLM validators run in consensus on a single piece of content. Use when correctness must be triple-checked before publishing." tasks: - type: validate id: fact_check aggregation: consensus validators: - agent: one-agent validation_rule: "Check if the numerical claims are plausible and internally consistent" - agent: one-agent validation_rule: "Check for logical consistency — identify any contradictions" - agent: one-agent validation_rule: "Check completeness — are all key financial metrics covered" success_criteria: "Content must be factually plausible, logically consistent, and comprehensive" input: "Tesla reported Q4 2025 revenue of $25.7B. The automotive segment contributed $21.3B while energy generation and storage added $2.8B. Gross margins improved to 19.8% from 17.6% year-over-year. The company delivered 495,000 vehicles in Q4, a 12% increase from Q3." ``` </Accordion> <Accordion title="05-batch-items.yaml"> ```yaml 05-batch-items.yaml name: "Per-Item Batch Pipeline" version: "1.0.0" description: "Run the same two-step pipeline for every item in a list, then roll up." tasks: - type: "batch" id: "per_company" items: - "Cloudflare" - "Vercel" - "Fly.io" tasks: - type: "task" id: "research" agent: brave-agent prompt: "{item} — company overview, funding, latest news" - type: "task" id: "brief" agent: one-agent prompt: "From the research above, write a 5-bullet analyst brief on {item}" - type: "task" id: "rollup" agent: one-agent prompt: "Combine the briefs into one comparison table: {data:per_company}" input: "edge platforms" ``` </Accordion> <Accordion title="06-harness-mission.yaml"> ```yaml 06-harness-mission.yaml name: "Bounded Research Mission" version: "1.0.0" description: "A goal-driven harness run with an inline mission, effort preset, and turn cap." tasks: - type: "harness" agent: "one-agent" id: "market_scan" goal: "Map the top 5 vendors for {input}, with pricing and one differentiator each." mission: mission: "You are a market analyst. Research thoroughly, cite sources, stay on topic." effort: "standard" max_turns: 12 success_criteria: - "5 vendors named with pricing" - "every claim has a source URL" - type: "output" id: "deliver" format: "markdown" input: "workflow orchestration platforms" ``` </Accordion> </AccordionGroup> # Workflows plane — /v1/workflows Source: https://www.plungeai.com/docs/skills/plungeai-workflows/references/api <!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/workflows.ts, orchestration/api-gateway/openapi-bots.ts, orchestration/api-gateway/docs/guide-3.0/16-bots.md, docs/ONE-API-DEVELOPER-GUIDE-2.0.md | last-synced: 2026-10-05 (every body field, event and status re-checked against GET /v1/openapi.json and routes/workflows.ts) --> Run multi-agent CNL workflows: **inline** (send the workflow document in the request), **saved** (reference a workflow id saved in Ocean Studio), each with an optional **SSE streaming** variant, plus a results-redemption route. Auth: `Authorization: Bearer ozk_YOUR_KEY` on every route here. Tenancy is automatic — the engine keys every run to your key's identity; saved workflows and results resolve only within it. Authoring CNL YAML itself (task types, `parallel`/`sequential`, harness missions) is this `plungeai-workflows` skill's job; this file covers the HTTP surface for executing it. ## POST /v1/workflows/execute — inline execution Two body formats: **JSON** — `{"workflow": <object or YAML string>, "input"?: string, "inputs"?: object, "user_context"?: object, "format"?: "json"|"yaml"|"markdown"|"text"}`. Bare root `name` + `tasks` nest into a workflow, and a bare `agent: <id>` with that agent's own fields lifts to a one-task workflow (`agent` next to `workflow` or `tasks` is `400 ambiguous_body`; a field named like an engine task key is `400 reserved_field`). `format` (or `Accept`) picks how the answer is written; raw YAML in gets YAML out: ```bash curl -s -X POST https://api.plungeai.com/v1/workflows/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow": { "name": "demo", "tasks": [ {"type": "task", "id": "t1", "agent": "one-agent", "prompt": "Say: ok"} ] }, "input": "x" }' ``` **Raw YAML** — send the whole workflow document as the body with `Content-Type: text/yaml`: ```bash curl -s -X POST https://api.plungeai.com/v1/workflows/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: text/yaml" \ --data-binary @- <<'YAML' workflow: name: demo tasks: - type: task id: t1 agent: one-agent prompt: "Say: ok" YAML ``` Response — HTTP 200 (an **acknowledgement + pointer**; once the synchronous run has stored its result, `content` carries the final task's output too): ```json { "success": true, "status": "completed", "workflow_id": "00000000-0000-4000-8000-000000000001", "final_task_id": "t1", "request_id": "00000000-0000-4000-8000-000000000004", "content": "ok" } ``` `status` is `completed`, or `incomplete` when the final task stopped at its turn budget (the result is partial). Redeem any task's output with `workflow_id` + that task id (below); for a multi-task workflow `final_task_id` is the last task and intermediate results are redeemable by their own task ids. The `workflow_id` is the execution id: it is your `x-trace-id` when you sent one, and every `/v1/executions/{id}` route takes it. Errors (envelope `{"error":{"code","message"}}`, header `X-Error-Code`): `400 missing_workflow` (none of `workflow`, `agent`, `tasks`, or an empty YAML body), `ambiguous_body`, `reserved_field`, `invalid_json`, `invalid_yaml`, `invalid_format`, `invalid_workflow` (the engine's structural check; the message names the problem); `403 agent_not_active` and `404 unknown_agent` (a named agent is parked or not in the registry — refused before dispatch, re-discover the id); `409 duplicate_execution_id` (a reused `x-trace-id`); `413 payload_too_large` (over 1 MiB); `424` (a connection-tier agent with no connection, `error.connect` says where to make it); `429 rate_limited`; `502 engine_error`. ## POST /v1/workflows/{id}/execute — saved workflow Runs a workflow, agent or bot saved under your account (the id shown in Ocean Studio, or returned by `POST /v1/workflows`). Body is optional: `{"input"?: string, "inputs"?: object, "user_context"?: object, "format"?}`. ```bash curl -s -X POST https://api.plungeai.com/v1/workflows/wf-morning-brief/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"input": "today"}' ``` ```json { "success": true, "status": "completed", "workflow_id": "00000000-0000-4000-8000-000000000001", "final_task_id": "t3", "request_id": "...", "content": "…" } ``` Errors: `404 workflow_not_found` (no such id under your identity — `GET /v1/workflows` lists yours), `404 unknown_agent` / `403 agent_not_active` (the saved YAML names an agent that is gone or parked), `400 invalid_format`, `409 duplicate_execution_id`, `413`, `429`, `502 engine_error`. `on_behalf_of` is refused here (`400 invalid_body`): only `POST /v1/execute` takes it. ## SSE streaming — live workflow events Two variants, same event stream: - `POST /v1/workflows/{id}/execute-stream` — saved workflow; body `{input?, inputs?, user_context?}` - `POST /v1/workflows/execute-stream` — inline body (`{workflow, input?, inputs?, user_context?}` or raw YAML), same shapes as `/v1/workflows/execute`; all of `input`, `inputs` and `user_context` are forwarded. ```bash curl -N -X POST https://api.plungeai.com/v1/workflows/wf-morning-brief/execute-stream \ -H "Authorization: Bearer ozk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"input": "today"}' ``` Response is `text/event-stream` with **named events** — each frame is `event: <type>` + `data: <json>`, and the data payload always carries `{workflow_id, execution_id, timestamp, type, data, elapsed_ms, duration_ms?}` (the event-specific fields live under `data`). Event sequence: ``` request_received → workflow_loaded → workflow_started → task_dispatched → task_completed (per task) → heartbeat (every ~25s during a task >20s — expect it, or idle timeouts will kill long streams) → workflow_completed (success only; execution summary + metrics) → workflow_result (terminal, ALWAYS check its success field) ``` On failure the engine emits `workflow_error` (error + progress counters) and the terminal `workflow_result` carries `success: false` + `error` — code that only checks for the `workflow_result` event name reports failed runs as done. Other event types appear for the matching task types (`parallel_start`, `batch_progress`, `debate_*`, …) — switch on the ones you need, ignore the rest. A refusal before the first frame (a body that does not parse, an unknown or parked agent, a missing saved workflow) is a plain JSON error envelope with the matching status (`400 invalid_workflow` / `invalid_yaml` / `missing_workflow`, `403 agent_not_active`, `404 workflow_not_found` / `unknown_agent`, `413`, `429`, `502 engine_error`), never an SSE frame — check the status before treating the body as a stream. Consume with any SSE client (`EventSource` won't do — it's a POST; use `fetch` + a stream reader in JS, or `curl -N` / `httpx.stream` elsewhere) and parse the `event:` lines, not just `data:` lines. The stream is pass-through and unbuffered; `x-request-id` is stamped on the response headers. Don't rely on a dropped stream's run surviving — the engine runs the workflow inside the stream; for durability use `/v1/workflows/execute` + the results route. ## GET /v1/workflows/results/{workflowId}/{taskId} — redeem a task result ```bash curl -s https://api.plungeai.com/v1/workflows/results/00000000-0000-4000-8000-000000000005/t1 \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` Ready — HTTP 200: ```json { "content": "ok", "content_type": "text/plain", "workflow_id": "00000000-0000-4000-8000-000000000001", "task_id": "t1" } ``` Not ready — HTTP 404 `{"error":{"code":"not_ready",...}}`. Poll with backoff (2s+, growing); `not_ready` while a workflow runs is normal. A run that failed is `409 execution_failed` (the message is the run's error); one that finished with no visible content is `422 empty_completion` (raise `max_tokens` to 64 or more). `?format=` (`json`, `yaml`, `markdown`, `text`) picks the wire. ## POST /v1/workflows/executions/{id}/cancel — cancel a running execution Cooperative cancel: writes the SharedMemory cancel marker the engine and agent loops poll at their next turn/tool boundary, then stamps the execution row `cancelled`. `{id}` is the execution id — the same value returned as `workflow_id` from `/v1/workflows/execute` (or the `x-trace-id` you sent on that call). Since `/v1/workflows/execute` is synchronous, send the cancel from a second connection while the first is still running. ```bash curl -s -X POST https://api.plungeai.com/v1/workflows/executions/00000000-0000-4000-8000-000000000005/cancel \ -H "Authorization: Bearer ozk_YOUR_KEY" ``` ```json { "success": true, "cancelled": "00000000-0000-4000-8000-000000000005", "markers": ["00000000-0000-4000-8000-000000000005"] } ``` Errors: `404 not_found` (no such execution under your identity — wrong owner never confirms the execution exists), `409 not_running` (already finished or already cancelled). `POST /v1/executions/{id}/cancel` is the same operation. ## Saved workflows, versions and bots — `/v1/workflows` CRUD (One API 2.3.0) The same rows Studio's sidebar and MCP `plungeai_workflow` show: | Route | Does | |---|---| | `POST /v1/workflows` | save `{name, kind, yaml, description?, folder?, campaign_config?}`; `kind` is `workflow`, `agent`, `bot` or `campaign` (`campaign_config` with `kind: campaign` sets up its ledger and schedule); `201 {workflow, warnings}` | | `GET /v1/workflows` | list yours `{workflows, total, limit, offset, has_more}` (`kind`, `search`, `folder`, `limit` 1–200 default 50, `offset`) | | `GET\|PATCH\|DELETE /v1/workflows/{id}` | read (row + live YAML), change `name`/`description`/`yaml`/`kind`/`folder` (`null` clears `description` and `folder`), delete (`204`) | | `GET\|POST /v1/workflows/{id}/versions` | list versions; save the current YAML as one `{label?}` | | `POST /v1/workflows/{id}/versions/{versionId}/restore` | make a version live again (recorded as a new version) | The YAML is validated against the CNL schema and the kind's lint BEFORE anything is written (`422 invalid_workflow` with `errors` and `warnings`, row unchanged) — also on `PATCH` and `restore`. A bot also has `…/permissions`, `…/workspace-files`, `…/inbox` and `…/bindings` (policy, files, inbox, chats) and `/v1/bot-groups`: see the verb table in `SKILL.md` and `plungeai-api-setup` → `references/bots-and-door.md`. Running a saved bot by id through `POST /v1/execute` with `agent: <id>@n` is the Agent Call door (`plungeai-api-setup` → `references/bots-and-door.md`). ## Fire-and-redeem pattern (Python) ```python import time, requests BASE = "https://api.plungeai.com" H = {"Authorization": "Bearer ozk_YOUR_KEY"} ack = requests.post(f"{BASE}/v1/workflows/execute", headers=H, json={ "workflow": {"name": "demo", "tasks": [ {"type": "task", "id": "t1", "agent": "one-agent", "prompt": "Say: ok"}]}, }).json() wf, task = ack["workflow_id"], ack["final_task_id"] for i in range(1, 9): r = requests.get(f"{BASE}/v1/workflows/results/{wf}/{task}", headers=H) if r.status_code == 200: print(r.json()["content"]); break time.sleep(2 * i) ``` ## Trace correlation Send `x-trace-id: <your-id>` on any execute call; every engine span of the run (dispatches, completions, the result) lands under that id at `GET /v1/traces/<your-id>` — see `plungeai-results-traces`. ## Which route for what CNL YAML (inline or saved) runs on `POST /v1/workflows/execute`, `POST /v1/workflows/{id}/execute` and their `-stream` twins. `POST /v1/execute` is the **Agent Call door**, not a workflow alias: the body is the program (a bare task, a `workflow:`, or `agent: <saved id>@n`), current and supported — `plungeai-api-setup` → `references/bots-and-door.md`. The pre-router CNL aliases of the SDK era are deprecated and not in the spec; new and generated code uses the routes above. # Getting a workflow into PlungeAI — three channels Source: https://www.plungeai.com/docs/skills/plungeai-workflows/references/channels <!-- sources-of-truth: orchestration/mcp-gateway/tools.ts, orchestration/api-gateway/openapi.ts | last-synced: 2026-10-05 --> ## Channel 1 (preferred): PlungeAI MCP server Connect once (self-service `ozk_` key from **Dashboard → One API → Keys**, `https://dashboard.plungeai.com`): ```bash claude mcp add --transport http plungeai https://mcp.plungeai.com/v1 \ --header "Authorization: Bearer ozk_YOUR_KEY" ``` Other MCP clients (Claude Desktop, Cursor, …) use the `mcpServers` JSON form: ```json { "mcpServers": { "plungeai": { "command": "npx", "args": [ "mcp-remote", "https://mcp.plungeai.com/v1", "--header", "Authorization: Bearer ozk_YOUR_KEY" ] } } } ``` Then the whole loop is tool calls — no copy-paste: 1. `plungeai_list_agents {search: "<capability in plain words>"}` — hybrid semantic search over the live catalog. Take agent ids ONLY from results; fetch a full card with `{agent_id}` before using an unfamiliar agent (cards carry parameters and "Not for → use X instead" redirects). 2. `plungeai_execute_workflow {workflow_yaml: "<draft>", input: "..."}` — test ad-hoc before saving. Use `mode: "async"` + `plungeai_get_workflow_status` for runs longer than ~3 minutes. 3. `plungeai_workflow {action: "create", name, yaml, description}` — the platform re-validates server-side, then saves and syncs live to Studio and peer apps. A refusal lists field-level errors — fix exactly those and retry. **This is your final validation step.** 4. `plungeai_workflow {action: "save_version"}` before big edits; `action: "update"` to iterate. 5. `plungeai_learn {source: "<distilled findings>"}` — save research from this chat as a reusable platform skill. ## Channel 2: paste into Ocean Studio No MCP server? Produce the final YAML in a fenced block for the user. In Ocean Studio: open (or create) the workflow and switch to the **Code** tab → paste the YAML → the editor offers **Validate YAML Syntax** and **Format YAML** → save. Errors surface inline. ## Channel 3: direct HTTP API (advanced) Same gateway, raw MCP JSON-RPC over HTTP. Get a self-service `ozk_` key from **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`). ```bash # list available tools curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer ozk_YOUR_KEY" -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' # execute an ad-hoc workflow (YAML goes in as a JSON string) curl -s https://mcp.plungeai.com/v1 \ -H "Authorization: Bearer ozk_YOUR_KEY" -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"plungeai_execute_workflow","arguments":{"workflow_yaml":"name: quick\ntasks:\n - agent: brave-agent\n prompt: \"{input}\"\n","input":"latest AI news"}}}' ``` Create/save is the same shape with `"name":"plungeai_workflow"`, `"arguments":{"action":"create","name":"…","yaml":"…"}`. `X-API-Key: ozk_YOUR_KEY` works as an alternative header. Rate limits by tier: free 30/min, pro 100/min, enterprise 300/min. # CNL Workflow Reference (engine v6) — PlungeAI Source: https://www.plungeai.com/docs/skills/plungeai-workflows/references/cnl-spec <!-- sources-of-truth: orchestration/cnl-engine/schema-types.ts, orchestration/cnl-engine/validate.ts | last-synced: 2026-10-05 --> CNL (Cognitive Natural Language) is PlungeAI's YAML workflow language: you declare tasks and how they compose (parallel, sequential, batch, debate, harness, …), and the engine dispatches them to platform agents with automatic data flow between steps. This reference is generated for engine v6 and is mechanically drift-checked against the engine source — trust it over older docs. ## Workflow shape (choose ONE) **Form A — wrapped** ```yaml workflow: name: string # required version: "1.0.0" # optional description: string # optional inputs: # optional — named variables, accessible as {key} key: value tasks: [Task] # required, ≥1 ``` **Form B — flat** ```yaml name: string version: "1.0.0" description: string tasks: [Task] input: string # optional single-input (backwards-compat), accessible as {input} ``` Both forms are accepted everywhere (ad-hoc execution auto-wraps Form B). A `followup:` block may exist in saved workflows but is ignored by the engine. **Fields every task may carry:** `id` (unique string — needed if later tasks reference its result), `type` (defaults to `task`), `description`, `condition` (CEL expression evaluated BEFORE dispatch; task is skipped when false), `location: cloud | local` (default cloud; `local` routes to the user's connected desktop daemon). ## Task types (10) ### `task` — single agent call Required: `agent` (kebab-case registry id) plus `prompt` (AI agents) OR `query` (search agents). All three must be strings when present. Optional: `retry: 0-2` — re-dispatches the same task on failure. **Idempotency warning:** a retry re-fires the FULL agent call; if the agent's side effect (payment, message send, external mutation) completed before the failure was reported, the retry duplicates it. Set retry only on read-only/idempotent agents; leave it 0 for payment-, messaging-, and automation-class agents. Retry applies ONLY to plain `task` — never to `harness`. Agent-specific fields pass through untouched (e.g. `operationType` for exa-agent, `max_results` for search agents, `provider`/`model`, `skills`/`experts` and `digital_twin` — legacy alias `persona` — for one-agent). Do NOT put `subtasks` on a `task`. These near-miss keys are warned as mistakes: `input`, `instructions`, `instruction`, `message` (you meant `prompt`) and `goal` (belongs to `harness`). ```yaml - type: "task" id: "research" agent: brave-agent prompt: "{input} — latest developments" ``` ### `parallel` — concurrent execution Required: `subtasks: [Task]` with ≥1 entry (`tasks:` is an accepted alias for `subtasks:`). ALL subtasks dispatch at once. The block itself must not carry `agent`/`prompt`/`query` — those belong on subtasks. ### `sequential` — serial with data chaining Required: `subtasks: [Task]` with ≥1 entry (`tasks:` alias accepted). Each task automatically receives the previous task's output, and any prior task's result is addressable as `{data:task_id}`. The block itself must not carry `agent`/`prompt`/`query`. ### `dynamic` — generator + executors Required: `generator: { agent, prompt? }` (agent string required) and `executors: [{ agent }]` (≥1, each with an agent string). Optional: `max` — positive integer cap on generated items. The generator produces a list; each executor runs per item. No `subtasks` here, and no root-level `agent`/`prompt`/`query`. ```yaml - type: "dynamic" id: "fanout" generator: { agent: one-agent, prompt: "List the top competitors of {input}, one per line" } executors: - agent: brave-agent max: 5 ``` ### `batch` — per-item pipeline over a list Required: `tasks: [Task]` (≥1 — the per-item pipeline; batch uses `tasks`, not `subtasks`) and either `items: [...]` (non-empty static list of strings or objects) or `items_from: "task_id"` (a prior task's result provides the list). Optional: `track: true` — persists per-item status and resumes completed items on re-run (off by default). Inside the pipeline, `{item}` is the current item (objects are JSON-stringified) and `{item.field}` addresses object fields. Data chains automatically between the pipeline's tasks — write follow-up prompts against the flowing data (e.g. "Summarize the research above for {item}"), not `{data:...}` (per-item task ids are rewritten internally). No `generator`/`executors` here — that's `dynamic`. ### `output` — final delivery Optional: `format: html | text | markdown`. The source content is auto-resolved from the flowing data; other fields are agent-specific. Use as the last task to shape the deliverable. ### `debate` — multi-perspective reasoning Required: `debaters: [{ agent, position?, digital_twin? }]` with ≥2 debaters (each needs an agent string; legacy alias `persona`). Optional: `judge: { agent, prompt? }` (agent string required when present), `rounds: 1-5` (integer, default 1). No `subtasks`/`generator`/`executors`/`items` on a debate. ### `validate` — consensus / majority / pass-fail (MACRO) Required: `validators: [{ agent, validation_rule? }]` (≥1, each with an agent string). Optional: `aggregation: consensus | majority | all_pass | any_pass` (default consensus), `success_criteria: string` (injected into all validator prompts). This is a macro: the engine expands it into a parallel block of PASS/FAIL validator tasks plus one judge task (`<id>_verdict`) carrying the aggregation rule. The hand-written expanded form is equally valid. No `subtasks`/`generator`/`executors`/`items`. ### `agent_loop` — legacy bounded loop Exists for backwards compatibility; dispatches to the loop runtime with a recursion depth guard. **Prefer `type: harness`** — it is the supported, validated form of the same idea. ### `harness` — mission-bounded agent run (the flagship) One goal-driven run of the loop runtime. Always set `agent: one-agent` — a harness task with no `agent:` still lands on the legacy `harness-agent` id until the switch-over. Use ONE harness task for open-ended work instead of many small tasks. Required: - `goal` — the per-run input, a string (`prompt:` is an accepted authoring alias and is folded into `goal`). - A mission, in one of three forms: 1. inline purpose text: `mission: "You are a market analyst. Stay on topic."` (any string with whitespace) 2. inline object (fields below) 3. a pre-built agent card reference: `mission_ref: card-id` (alias `pack:`, or a single-token `mission:` value). If BOTH a card ref and an inline object are set, the inline values override the card per key (the engine warns). Inline mission object fields (all optional except `mission`): | Field | Meaning | |---|---| | `mission` | Standing purpose → system-prompt frame (required in object form) | | `persona` | Persona / digital-twin id — one voice per run (single string) | | `skills` | Skill ids, array — injected eagerly (budget-capped), rest loadable in-loop | | `experts` | Expert/specialist ids, array — labeled context sections | | `backgrounds` | Always-on ambient context card ids, array | | `plugins` | Plugin bundle ids, array — expand to skills + mcp + scripts | | `mcp` | MCP server ids, array — connected in-loop | | `model` / `provider` | Model/provider override | | `effort` | `quick` \| `standard` \| `deep` — turn/parallel budget preset | | `python_executor` | `auto` \| `pyodide` \| `anthropic` \| `gemini` — run_python routing | | `instructions` | Extra instructions appended after skills | | `allowed_tools` | Tool fence, array — only these reach the LLM; omitted → on `one-agent` every tool (the 8-tool default fence is `universal-agent`'s only) | | `allowed_agents` | Agent fence for call_agent: array of ids, or `'all'` | | `denied_agents` | Array — subtracted from `allowed_agents: all` | | `success_criteria` | Array of self-checked statements | | `max_turns` | Loop turn cap, positive number (`max_iterations` is a legacy alias; optional — effort/agent default applies when absent) | | `max_parallel` | Widest fan-out one delegate call may spawn (cap-and-refuse) | | `max_tokens` | Per-turn OUTPUT token budget for the loop's model calls (runtime default 16384) | | `budget_usd_run` | Per-run spend cap in USD — the loop prices its running token cost each turn and stops (`stopReason: 'budget'`) once it exceeds this; absent = no cap | | `permissions` | Map of tool name OR permission class (`send` \| `write` \| `pay` \| `delete`) → `allow` \| `ask` \| `deny` \| `hand_off`. `ask` pauses the run for human approval; `hand_off` is a deny that does not retry — the call never runs and the result tells the owner it is theirs to do (`one-agent` only; any other runtime gets `deny`). A saved bot's stored presets merge under these, per class | | `permission_locks` | Admin-locked permission classes, array — a class listed here forces its `allow` up to `ask` so a bot author's own `allow` cannot silently auto-run it; sourced from the Studio execute boundary, never trusted from raw YAML for enforcement | | `memory_owner` | Memory namespace override — **do not use; omit entirely** (policy 2026-08-23). Never put a user id in YAML: ownership lives on the workflow row and runtime identity is injected per run; a hardcoded id that isn't the runner's own is rejected by the harness guard anyway. See `orchestration/BOT-CREATION.md`. | | `local` | `true` = this run's actions execute on the user's own computer via the desktop daemon (cloud brain, local hands; requires a connected local node) | | `local_agents` | Which local agents may be used (array; absent + `local: true` → all connected) | | `peers` | Bots this bot may message with `message_bot`, array of bot names/ids (`a2a:<url>` entries are listed verbatim); fail-closed — absent or empty and `message_bot` refuses | | `posture` | `read_only` — denies the write / send / delete / pay classes; the deny beats the permission floor's `ask`, so nothing is even offered for approval | | `review` | `auto` — a separate reviewer model checks each send / write / pay / delete call and answers allow / ask / deny; 3 consecutive denies stop the run | | `max_runtime_s` | Wall-clock cap in seconds — the run stops at the next turn boundary with `stopReason: 'timeout'`; absent = no wall-clock cap | | `workspace` | `run` \| `bot` — file workspace (VFS) scope: a saved bot gets a per-bot persistent workspace by default; `run` keeps files per run | Mission fields may also be written FLAT on the task (peers of `goal:`) — the engine folds them into the mission. Validation errors you'd hit: missing/non-string `goal` ("Type 'harness' requires a 'goal' string"), no mission or card ("requires a 'mission' … or a 'mission_ref'"), object mission without a purpose string, non-array `allowed_tools`, non-positive `max_turns`, `effort` outside quick/standard/deep, capability fields (`skills`/`experts`/`backgrounds`/`plugins`/`mcp`) not arrays of strings, `persona` not a single string. ```yaml - type: "harness" agent: "one-agent" id: "market_scan" goal: "Map the top 5 vendors for {input}, with pricing and one differentiator each." mission: mission: "You are a market analyst. Research thoroughly, cite sources, stay on topic." effort: "standard" max_turns: 12 success_criteria: - "5 vendors named with pricing" - "every claim has a source URL" ``` ## Interpolation - `{input}` — the primary input (`input:` at the workflow level, or the input given at execution) - `{key}` — any named variable declared in `inputs:` - `{data:task_id}` — result of a prior task by id - `{item}` / `{item.field}` — current item inside a `batch` pipeline Data flows automatically between sequential tasks — don't hand-wire results. ## Hard rules - Agent names MUST match the live registry exactly (kebab-case) — discover them via registry search, never from memory. Do NOT use `brave-search` — use `brave-agent`. Execution refuses unknown or inactive agent ids. - Never use `$variable`, `depends_on`, `outputs:`, or `parallel: true` — these don't exist. - `parallel` / `sequential` / `batch` / `dynamic` / `debate` / `validate` blocks MUST NOT carry an `agent` field directly — agents go on the inner tasks. - `type: task` reads `prompt:` (or `query:`) — not `input`, `instructions`, `instruction`, or `message`. `goal` belongs to `type: harness` only. `subtasks` never belongs on a `task`. - Every `id` must be unique within its scope. - Long prompts use YAML block scalars (`prompt: |`) — never hard-wrap a value; quote values containing colons. - `digital_twin:` (legacy alias `persona:`) only works on `one-agent` and on `debater`/`validator`/`judge` entries. - Nesting is unlimited: parallel inside sequential inside parallel, etc. Keep it under ~4 levels for sanity. - Scheduling is a scheduler job (Studio, `plungeai_schedule`, `/v1/schedules`, `ocean-cli schedules`), not YAML — there is no `schedule:` field. ## Validation errors you may see | Message (verbatim) | Fix | |---|---| | `Workflow name is required and must be a string` | Add a top-level `name:` | | `Workflow must have at least one task` | Add a `tasks:` list | | `Invalid task type: X. Must be 'task', 'parallel', 'sequential', 'dynamic', 'batch', 'output', 'debate', 'validate', 'agent_loop', or 'harness'` | Use one of the 10 types (or omit `type:` for a plain task) | | `Type 'harness' requires a 'goal' string` | Add `goal:` (or `prompt:` alias) to the harness task | | `Type 'harness' requires a 'mission' (purpose text or inline object) or a 'mission_ref' (pre-built agent card id)` | Add a mission — text, object, or card ref | | `Type 'parallel' requires a subtasks array` | Put the inner tasks under `subtasks:` | | `Type 'dynamic' requires a generator object with at least an 'agent' field` | Add `generator: { agent: ... }` | | `Type 'batch' requires either 'items' (array) or 'items_from' (task ID string)` | Provide the item source | | `Type 'debate' requires at least 2 debaters` | Add a second debater | | `Rounds must be a positive integer between 1 and 5` | Fix `rounds:` | | `Aggregation must be one of: consensus, majority, all_pass, any_pass` | Fix `aggregation:` | | `Type 'task' does not read 'input' — did you mean 'prompt'?` (warning) | Rename the field to `prompt:` | | `Type 'task' should not have subtasks (use 'parallel' or 'sequential')` (warning) | Change the type or move subtasks | | `Max must be a positive integer` | Fix `max:` on the dynamic block | # Workflows — execute_workflow, get_workflow_status, list_workflows, workflow, build_workflow, executions Source: https://www.plungeai.com/docs/skills/plungeai-workflows/references/mcp <!-- sources-of-truth: orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/server.ts, orchestration/mcp-gateway/chat.ts, orchestration/mcp-gateway/workflow-store.ts, orchestration/mcp-gateway/conversation.ts, orchestration/mcp-gateway/extras.ts | last-synced: 2026-10-05 (every param and action re-checked against the zod schemas in tools.ts; kind filter on list_workflows, the format arg) --> Workflows are CNL YAML run by the platform engine. Authoring guidance (task types, recipes, validation rules) lives in this **plungeai-workflows** skill — this file covers the MCP tools that run, manage, and observe them. Everything these tools create syncs live to Studio and peer apps (one account, same data, every surface). --- ## plungeai_execute_workflow **Purpose:** execute a CNL workflow — either a saved one (`workflow_id`) or ad-hoc YAML (`workflow_yaml`). One of the two is required. **Parameters** | Param | Type | Notes | |---|---|---| | `workflow_yaml` | string ≤256 KiB | Ad-hoc CNL. The `workflow:` wrapper is optional — bare top-level `name:`/`tasks:` is wrapped automatically, and a bare agent call (`agent: <id>` plus that agent's own fields) lifts to a one-task workflow. `format`, `input`, `inputs`, `stream`, `sync`, `dry_run` and `metadata` at the root are wire keys, never agent fields. | | `workflow_id` | string | A saved workflow the caller owns (from `plungeai_list_workflows`). | | `input` | string ≤65536 | Fills `{input}` placeholders; also becomes the run's display title. | | `inputs` | map string→string (values ≤8192) | Named inputs for multi-input workflows. | | `user_context` | object | Where/when the user is, merged over the network guess: `time_zone` (IANA), `locale`, `place`, `latitude`, `longitude`, `accuracy_m`, `source` (`device` default \| `network`). Validated; a bad value is dropped. (MCP 2.6.0; every execute tool takes it, and the Agent Call door takes it as a root key) | | `mode` | `sync` \| `async` | Sync (default) streams progress and returns the result. Async returns an `execution_id` immediately — use for runs that could exceed ~3 minutes. | | `format` | `markdown` (default) \| `json` | `json` returns the same outcome as a JSON document in `text` plus `structuredContent` (every result-bearing tool takes it). | **Example (ad-hoc, sync)** ```json {"user_request": "get me the latest AI news", "workflow_yaml": "name: quick\ntasks:\n - agent: brave-agent\n prompt: \"{input}\"\n", "input": "latest AI news"} ``` **Example (saved, async)** ```json {"user_request": "run my due diligence workflow on Acme", "workflow_id": "wf-1234...", "input": "Acme Corp", "mode": "async"} ``` **Returns:** an outcome envelope. Sync `ok` → the final output + execution-id footer; a paused run → `needs_approval`/`needs_input` with the ⏸ block. Async → "Started execution `<id>` (async). Poll plungeai_get_workflow_status ..., then fetch output with plungeai_get_result. If the status reports ⏸ AWAITING USER APPROVAL or AWAITING USER, relay it to the user and continue with plungeai_continue." — follow that script literally. **Failures & fixes** - Neither `workflow_yaml` nor `workflow_id` → `needs_input`; provide one. - "No saved workflow with id ... on this account." → list with `plungeai_list_workflows` and use a real id (ownership is enforced). - Ad-hoc YAML naming an unknown/inactive agent → `unavailable` refusal BEFORE dispatch (the fence walks nested `parallel`/`sequential` blocks too). Re-discover the agent id; a refusal writes no execution row. - "Invalid YAML" → the envelope carries a repair tip: quote any value containing a colon (`prompt: "DD: memo"`), write long strings as block scalars (`prompt: |`) and never hard-wrap a value — or compose with `plungeai_build_workflow` instead. - Timeout in sync mode → remediation says `retry_with {mode: "async"}`; do that rather than re-running sync. --- ## plungeai_get_workflow_status **Purpose:** check one of the caller's executions. Self-heals stuck rows: a "running" row whose result already landed flips to `completed`; a run silent for over 15 minutes flips to `failed` ("Interrupted") — so a dead async run never reads "running" forever. Read-only. **Parameters:** `execution_id` (required). Engine ids (`exec-...`, hyphen) resolve too; scheduler run ids (`exec_...`, underscore) are redirected to `plungeai_schedule {action: "runs"}`. **Example** ```json {"user_request": "is my report done yet?", "execution_id": "d3adb33f-..."} ``` **Returns:** markdown (workflow, status, error if failed, started, duration, final task) plus `structuredContent` a machine mirror: ```json {"execution_id": "...", "status": "running|completed|failed", "error_message": null, "workflow_name": "...", "started_at": "...", "duration_ms": 1234, "final_task_id": "...", "continuation": {"status": "needs_approval", "agent": "...", "question": null, "pending_action": {"summary": "...", "price": "..."}}} ``` `continuation` is non-null when the run is PAUSED awaiting the user (async pollers meet approvals here first). Its `status` is one of two values: `"needs_approval"` (the ⏸ AWAITING USER APPROVAL block; `pending_action` carries the summary/price) or `"question"` (the ⏸ AWAITING USER block; `question` carries the text). Either way: relay it and use `plungeai_continue`. `status: completed` → fetch output with `plungeai_get_result`. Full conversation/HITL protocol: `plungeai-results-traces`. **Failures & fixes:** "Execution not found." → wrong/foreign id (take it from a footer or the executions list). "Status storage is temporarily unavailable" → transient, retry in a moment. **Polling etiquette:** poll every few seconds for short runs, backing off for missions; stop on `completed`/`failed` or a non-null `continuation`. --- ## plungeai_list_workflows **Purpose:** the user's SAVED workflows — what users usually mean by "my agents". Read-only. **Parameters:** `search` (name contains), `folder` (folder name contains), `kind` (`workflow` \| `agent` \| `bot` \| `campaign`), `limit` (1–100, default 50), `format`. **Behavior:** with no filters, the answer opens with a folder overview (non-empty folders only, with counts) and a recency-ordered table — Name, Kind (`workflow`/`agent`/`bot`/`campaign`), Folder, ID, Description. With filters, just the matching table. **Example:** `{"user_request": "show my agents in the finance folder", "folder": "Finance"}` **Failures & fixes:** "No workflows found." → offer `plungeai_workflow {action: "create"}`, `plungeai_build_workflow`, or `plungeai_templates`. "No workflows matched." → list without filters to see real folder names. --- ## plungeai_workflow (CRUD + versioning) **Purpose:** manage saved workflows directly. Every write syncs live to Studio and peer apps. **Actions & required params** | Action | Requires | Notes | |---|---|---| | `create` | `name`, `yaml` | Server-side CNL validation first — a refusal lists field errors; fix exactly those. Optional `description`, `folder` (filed via create-then-move), `kind` (`workflow` \| `agent` \| `bot` \| `campaign` — the sidebar section). A `kind: campaign` create takes `campaign_config` (`{list: {inline\|table\|agent\|csv}, cycle, max_attempts, schedule?, deliver?, …}`) and sets up the ledger and its schedule. | | `get` | `workflow_id` | Returns name, description, and the full YAML block. | | `update` | `workflow_id` | Any of `name`, `description`, `yaml` (re-validated), `folder` (move; `"none"` or `""` clears), `kind`. | | `delete` | `workflow_id` | Removes it everywhere. | | `save_version` | `workflow_id` | Snapshot (optional `version_description`) — do this before big edits. | | `list_versions` | `workflow_id` | Version table (V#, id, when, note). | | `get_version` | `workflow_id`, `version_id` | That version's YAML. | | `restore_version` | `workflow_id`, `version_id` | Restores as a NEW version. | | `a2a_door` | `workflow_id`, `door_action` (`mint` \| `get` \| `pause` \| `resume` \| `revoke`), optional `handle` (mint only: 3–63 lowercase letters, digits, dashes; default the workflow's name) | The workflow's Agent2Agent (A2A 1.0) door: a URL under `https://a2a.plungeai.com/<handle>` with its own bearer token, shown once on `mint` (minting again rotates it); the agent card is at `<door url>/.well-known/agent-card.json`. A bot calls another A2A agent with `message_bot` and a `peers` entry `a2a:<url>`. | | `permissions_get` | `workflow_id` | A bot's stored policy: `classes` (`send`/`write`/`pay`/`delete` → `allow`/`ask`/`deny`/`hand_off`), `locks`, `budget_usd_run`, `budget_usd_month`. Owner only. (MCP 2.6.0) | | `permissions_set` | `workflow_id` + any of `classes`, `locks`, `budget_usd_run`, `budget_usd_month` | Fields sent replace, `null` clears a cap. `locks` need a team admin — anyone else gets the 403 `lock_admin_required` as an error and nothing is written. | | `files` / `file` | `workflow_id` (+ `path` for `file`) | List the bot's persistent workspace files / read one inline as base64 — up to 5 MB; a bigger file is an error that names the REST download route (`GET /v1/workflows/{id}/workspace-files/download?path=`, 20 MB). | | `inbox` | `workflow_id` | Queued bot-to-bot messages (read-only). | | `bindings` / `bind` / `unbind` | `workflow_id` (+ `channel`, `sender_id`) | Paired chats (Telegram, WhatsApp, Discord, Teams) that talk to this bot; the chat must be paired first. A chat talks to ONE bot. | | `groups` / `group_set` / `group_delete` / `group_thread` | `group` (the group's name) + for `group_set` `members` (2–6 of your bot ids) | Bot groups: named sets of 2–6 of your bots for `message_bot to=group:<name>`; the thread is read-only. | **Example** ```json {"user_request": "save this as a workflow called Daily Brief", "action": "create", "name": "Daily Brief", "yaml": "name: daily-brief\ntasks:\n - agent: brave-agent\n prompt: \"{input}\"\n", "description": "Morning news brief", "folder": "News"} ``` **Failures & fixes:** "Workflow YAML failed CNL validation: <field errors>" → fix exactly the named fields (see this skill's CNL rules), retry once. "Workflow not found." → not this user's id — re-list. Folder misses split by action: on **create** the miss is non-fatal (⚠️ "… created without a folder." — fix later with `update`); on **update** a folder miss FAILS the whole update — `No folder matched "<name>". Your folders: <list>` — and NONE of the other patch fields (name/yaml/kind/description) are applied. The error lists your real folder names: pick one and re-send the full update. Validation ⚠️ warnings are non-fatal — surface them. --- ## plungeai_build_workflow **Purpose:** have the platform builder generate (or refine) a workflow from natural language. It picks REAL registry agents, validates, self-repairs, and saves — the fastest correct path when the user describes an outcome rather than YAML. **Parameters:** either `goal` (new workflow) or `workflow_id` + `instruction` (regenerate an existing one with a change applied). **Example:** `{"user_request": "build me a workflow that researches a company and drafts an outreach email", "goal": "Research a company from its name, then draft a personalized outreach email"}` **Returns:** "Created workflow **<name>** (`<id>`). Synced to all apps. Run it with plungeai_execute_workflow." plus the full generated YAML. Expect 30–60s (a single generation call with an internal self-repair loop) — progress notifications cover the wait; don't re-call mid-generation. **Failures & fixes:** "Generated workflow failed CNL validation: ..." (rare — includes the YAML) → retry once with a sharper goal, or hand-fix the YAML and save via `plungeai_workflow {action: "create"}`. "Workflow not found" on refine → wrong `workflow_id`. --- ## plungeai_executions **Purpose:** browse and manage the caller's execution history. Outputs are final user-ready markdown — relay tables and threads verbatim. **Actions** | Action | Requires | Returns | |---|---|---| | `list` | — (optional `workflow_id` filter, `limit` default 20, `offset`) | Table: ID · Question (the run's own input) · Workflow · Status · When (newest first). | | `get` | `execution_id` | Same view as `plungeai_get_workflow_status`. | | `output` | `execution_id` | Same as `plungeai_get_result`. | | `conversation` | `execution_id` | The run's follow-up thread (all User/Agent turns). | | `delete` | `execution_id` | Removes the run row and its stored result (the run's follow-up conversation thread is not purged). | | `cancel` | `execution_id` | Cooperative stop of a RUNNING execution (at its next turn or tool boundary); a finished run answers that there is nothing to cancel. | | `steer` | `execution_id`, `message` | Queue a message for a RUNNING execution; the live loop reads it at its next turn (a finished run is an error: use `plungeai_followup`). (MCP 2.6.0) | | `files` | `execution_id` | The Office/PDF files the run produced (spreadsheets, documents, decks). (MCP 2.6.0) | **Example:** `{"user_request": "show my last 10 runs", "action": "list", "limit": 10}` **Failures & fixes:** "Execution not found." → foreign/wrong id. "No conversation thread for this execution yet." → the run had no follow-ups — offer `plungeai_followup`. "Conversation storage is temporarily unavailable" → transient, retry. **Numbered references:** when the user says "show me #2", resolve against the exact list YOU displayed earlier in the conversation and use that row's ID — never against a fresh `list`, because new runs shift the numbering. --- ## plungeai_templates — instantiating a template as a workflow (`action: "use"`) **Purpose:** the workflow-template gallery — browse, inspect, and turn a template into your own saved workflow. Browsing (`action: "list"`/`"get"`) is documented in the **plungeai-discovery** skill (`references/list-and-contract.md`); this section covers `action: "use"`, which is workflow authoring, not discovery — it creates a saved workflow you then run and iterate like any other. **Parameters** | Param | Type | Notes | |---|---|---| | `action` | `"list"` \| `"get"` \| `"use"` | `list`/`get` — see `plungeai-discovery`. | | `template_id` | string, required | The template to instantiate (from a `list`/`get` call). | | `name` | string ≤512 | The new workflow's name. Default `<template name> (copy)` — except a **bot**-kind template, which defaults to the template's own name with no "(copy)" suffix. | | `folder` | string | Files the new workflow into a folder by name. A miss is non-fatal — the workflow is still created, with a ⚠️ "created without a folder" note. | **Example** ```json {"user_request": "start from a research template", "action": "use", "template_id": "tpl-...", "name": "My research pipeline", "folder": "Research"} ``` **Returns:** creates a saved workflow that carries the template's `kind` (`workflow`/`agent`/`bot`) and its full YAML content, bumps the template's usage count, and replies "Created workflow **<name>** (`<id>`) from template **<template>**. ... Run it with plungeai_execute_workflow." Iterate the result afterward with `plungeai_workflow {action: "update"}` like any other saved workflow — `use` does not create a special kind of object. **Failures & fixes:** `use` without `template_id` → "template_id is required for this action." — supply the id from a `list` call. Unknown `template_id` → "Template not found: <id>" — re-list, ids can go stale. "Template content missing from storage." → a platform-side gap; report it rather than retrying. # Workflows — CNL orchestration: what it is and how to run it Source: https://www.plungeai.com/docs/skills/plungeai-workflows/references/overview <!-- sources-of-truth: orchestration/cnl-engine/README.md, orchestration/cnl-engine/schema-types.ts, orchestration/api-gateway/openapi.ts, orchestration/mcp-gateway/server.ts, apps/ocean-skills/skills/plungeai-workflows/SKILL.md | last-synced: 2026-10-05 (execute-route response, errors, plungeai_workflow kinds and actions) --> A **workflow** is a YAML document (CNL — Cognitive Natural Language) that the engine executes as a DAG of agent calls: parallel fan-out, sequential pipelines, unlimited nesting, conditions, batches, debates, and bounded autonomous missions. The engine is pure orchestration — it routes tasks to agents over RPC and coordinates data handoff; agents do the actual work. > **Authoring belongs to this `plungeai-workflows` skill** — full CNL spec, validation > rules, recipes, and runnable examples ([`references/cnl-spec.md`](/skills/plungeai-workflows/references/cnl-spec), `recipes.md`, > `examples/`). This reference is the platform-level view: what workflows can do, > the facts that shape good designs, and every way to execute one. Do not write > non-trivial YAML without the authoring skill loaded. ## Shape of a workflow ```yaml name: Research and synthesize tasks: - type: parallel id: research subtasks: - { type: task, id: web, agent: brave-agent, query: "{input}" } - { type: task, id: deep, agent: exa-agent, query: "{input}" } - type: task id: synthesize agent: one-agent prompt: "Synthesize the research above into a brief on: {input}" ``` Data flows automatically: each task's result lands in SharedMemory and downstream tasks receive upstream content — you never hand-wire results. Ten task types exist: | Type | One line | |---|---| | `task` | Single agent call (the default) | | `parallel` | All `subtasks` dispatched at once | | `sequential` | `subtasks` in order, each seeing prior results | | `dynamic` | Generator agent produces N items → executors fan out over them | | `batch` | Per-item pipeline over a list — isolated; per-item tracking + resume is opt-in `track: true` | | `debate` | N debaters, up to 5 rounds, optional judge | | `validate` | Macro → parallel PASS/FAIL validators + judge verdict | | `output` | Deliver a previous result (email/document) with a format transform | | `harness` | Bounded autonomous mission — see `plungeai-missions` | | `agent_loop` | Legacy alias for `harness` — prefer `harness` | Blocks (`parallel`/`sequential`/`batch`/`dynamic`/`debate`/`validate`) never carry `agent:` themselves; agents go on inner tasks. Conditions (`condition:` CEL, e.g. `input.contains('urgent')` or `qa_gate.verdict == "FAIL"`) gate any task or block before dispatch. ## Parallelism — the facts that should shape your designs These are structural (read from engine code) and measured (from the engine's own server-side event timestamps): - `type: parallel` dispatches **every** subtask at once — one `Promise.all`, no chunking, no width cap. Each child is a **separate Worker invocation** over RPC. - Engine overhead is **zero at every width** measured (1 → 100 children): dispatch spread 0 ms; workflow total equals the slowest child to the millisecond. A parallel block costs *the slowest of N*, not N of anything. The same 12 search tasks: sequential 18.9 s, parallel 1.9 s. - **I/O-bound fan-out is flat** — children waiting on networks do not contend, even against the same agent service. - **CPU-bound or isolate-heavy children queue at the destination**, not in the engine. Practical width against ONE shared destination: **~12-50** depending on the service; past that you are queueing, not gaining. To go wider, spread children across distinct destinations (a real research fan-out across six search providers does this naturally). - Every child's result is durable in SharedMemory — collect at the end or pick up each as it lands. - Measure with an A/B (`sequential` vs `parallel` of the same tasks), never by dividing a parallel total by a single-task average. ## Inputs and placeholders `{input}` is the primary input; `{input1}`…`{input10}` and any custom `{semantic_name}` also work — declared under `inputs:` and replaced in one pass. **Date tokens** (for rolling windows — scheduled runs especially): `{now}`, `{today}`, `{yesterday}`, `{week_start}`, `{week_end}`, `{last_week_start}`, `{last_week_end}`, `{month_start}`, `{month_end}`, `{last_month_start}`, `{last_month_end}`. All UTC, ISO weeks (Monday first), seeded from the run's execution time; a caller-supplied input of the same name wins. See `plungeai-scheduling` for the pattern. **Search-query guard:** search agents cap query length (per their cards) and an over-limit query — typed or auto-filled from a prior task's output — returns a structured `needs_input` refusal; nothing is silently truncated. Give search tasks short explicit queries; bulk upstream data flows as context, never as the query. ## Executing a workflow ### MCP (preferred when operating live) ``` # Ad-hoc (test before saving — always) plungeai_execute_workflow {workflow_yaml: "<yaml>", input: "solid-state batteries"} # Long run plungeai_execute_workflow {workflow_yaml: "<yaml>", input: "…", mode: "async"} # → execution_id → plungeai_get_workflow_status → plungeai_get_result # Saved workflow by id plungeai_execute_workflow {workflow_id: "…", input: "…"} ``` YAML gotchas the platform will refuse: hard-wrapped values (use block scalars `prompt: |`), unquoted values containing colons, invented fields (`depends_on`, `outputs:`, `parallel: true`, `schedule:` — scheduling is a separate system, see `plungeai-scheduling`). Or skip hand-writing: `plungeai_build_workflow {goal}` generates and saves one via the platform builder. Manage saved workflows with `plungeai_workflow` (`action: create|get|update|delete|save_version|list_versions|get_version|restore_version`, plus `a2a_door` and the bot verbs in `SKILL.md`) — create/update/delete sync live to Studio and peer apps. Create/update also take `folder` (file/move it — `"none"` clears) and `kind: workflow|agent|bot|campaign` (which sidebar section it lives in: Flows, Agents, Bot agents, or Campaigns). This is the mechanic behind the "my agents" terminology: a saved item with `kind: agent` IS the user's "agent", and `plungeai_list_workflows` filters by `folder`, `kind` and `search`. Server-side validation on save is your final gate: a refusal lists field errors; fix exactly those. Pre-built starting points: `plungeai_templates {action: list|get|use}`. ### One API ```bash # Inline (JSON wrapper or raw text/yaml body) curl -s -X POST https://api.plungeai.com/v1/workflows/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"workflow": "name: quick\ntasks:\n - agent: brave-agent\n query: \"{input}\"\n", "input": "latest AI news"}' # → {success, status, workflow_id, final_task_id, request_id, content} # Saved workflow by id (resolved against your user) curl -s -X POST https://api.plungeai.com/v1/workflows/{id}/execute \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"input": "latest AI news"}' # Live progress: SSE stream of engine events curl -N -X POST https://api.plungeai.com/v1/workflows/{id}/execute-stream \ -H "Authorization: Bearer ozk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"input": "…"}' ``` The acknowledgement carries a pointer, and `content` (the final task's output) once the synchronous run has stored it. Redeem any task's result: ```bash curl -s https://api.plungeai.com/v1/workflows/results/{workflowId}/{taskId} \ -H "Authorization: Bearer ozk_YOUR_KEY" # 200 {content, content_type, workflow_id, task_id} | 404 not_ready (still running) | 409 execution_failed | 422 empty_completion ``` `final_task_id` from the acknowledgement is the task id of the final result; any intermediate task id works too (read one branch of a fan-out). Errors: `400 missing_workflow` / `invalid_workflow` / `invalid_yaml`, `403 agent_not_active`, `404 workflow_not_found` / `unknown_agent`, `409 duplicate_execution_id`, `502 engine_error` ([`references/api.md`](/skills/plungeai-workflows/references/api) has the full list). ### Studio Humans edit and run workflows visually; the Code tab accepts pasted YAML with inline validation. Point users there for visual work and credential connects. ## Reading results - MCP: `plungeai_get_result {workflow_id}` → final output (full conversation thread for conversational runs); add `task_id` for a single step. Output is final user-ready markdown — relay verbatim. - One API: the `/results/` endpoints above. - Execution completed but "results not yet available" → retry `plungeai_get_result`; storage is eventually consistent by a beat. ## Follow-ups A completed execution can be continued conversationally: `plungeai_followup {execution_id, prompt: "<follow-up question>"}` — `prompt` is the required parameter (`message` belongs to `plungeai_continue`, the HITL resume tool — do not cross them). It reuses the run's context. Workflows may carry a `followup:` block (provider/model/memory_scope/temperature/max_tokens) which the engine ignores at execution time; it configures this follow-up behavior. Full conversation/HITL detail: `plungeai-results-traces`. ## Failure semantics worth knowing - Parallel branches are best-effort: a failed branch does not abort the run; the summary carries `failed_branches[]` (present only when something was lost). Check it before trusting a synthesis over fan-out results. - Per-task `retry: N` (max 2) exists for plain `type: task` only — never for `harness` — and re-fires the FULL agent call: set it only on read-only/idempotent agents, never payment/messaging/automation classes. - The execution summary (`total_tasks_executed`, `tasks_skipped`, `parallel_blocks_executed`, per-task durations) is your first debugging read — see `plungeai-results-traces`. # CNL recipes — pick the shape, then adapt Source: https://www.plungeai.com/docs/skills/plungeai-workflows/references/recipes <!-- sources-of-truth: orchestration/cnl-engine/schema-types.ts, apps/ocean-skills/skills/plungeai-workflows/examples/ | last-synced: 2026-10-05 --> Full runnable versions live in `examples/`. Two composition rules apply to all of them: - **Wrap independent work in `parallel`** — the engine fans out ALL subtasks at once over RPC; width is nearly free for I/O-bound work. - **Prompt-chaining beats mega-prompts** — pass `{data:task_id}` forward through small focused tasks instead of one giant prompt. ## 1. Single task (`examples/01-simple-search.yaml`) The smallest valid workflow: one agent, one prompt. Use for smoke tests and one-shot calls. ```yaml tasks: - type: "task" id: "single_task" agent: brave-agent prompt: "{input}" ``` ## 2. Parallel research + synthesis (`examples/02-parallel-research.yaml`) **The most common production pattern.** Independent searches from different engines fan out at once; one LLM task synthesizes with `{data:...}` references. ```yaml tasks: - type: "parallel" id: "research_phase" subtasks: [ …brave-agent…, …tavily-agent…, …exa-agent… ] - type: "task" id: "synthesis" agent: one-agent prompt: "Synthesize: {data:brave_search} {data:tavily_research} {data:exa_deep}" ``` ## 3. Hierarchical branches (`examples/03-hierarchical.yaml`) parallel → sequential → parallel nesting, unlimited depth. Use when branches have internal pipelines (e.g. search → per-branch analysis) that should still run side by side. ## 4. Consensus validation (`examples/04-validate-consensus.yaml`) Produce something, then have several validators vote (`aggregation: consensus | majority | all_pass | any_pass`). Use for quality gates on generated content. ```yaml - type: "validate" id: "quality_gate" validators: - { agent: one-agent, validation_rule: "claims are sourced" } - { agent: one-agent, validation_rule: "no speculation stated as fact" } aggregation: "majority" ``` ## 5. Per-item batch (`examples/05-batch-items.yaml`) Same pipeline for every item in a list (`items:` static or `items_from:` a prior task). `{item}` / `{item.field}` inside the pipeline; data chains automatically between the pipeline's tasks. Add `track: true` for resumable long lists. ## 6. Bounded harness mission (`examples/06-harness-mission.yaml`) Open-ended, goal-driven work = ONE `type: harness` task with a mission (purpose, effort, `max_turns`, `success_criteria`) — not ten hand-planned small tasks. The loop runtime plans, uses tools, and self-checks inside the fence you declare. Reach for it when the steps can't be enumerated up front. ## Choosing between `dynamic`, `batch`, and `harness` - Know the list already → `batch`. - An agent must generate the list first, then each item gets the same treatment → `dynamic` (`generator` + `executors`, cap with `max`). - The steps themselves are unknown and the agent must decide as it goes → `harness`. ## Debate (no example file — shape only) Two or more debaters argue positions, optional judge decides, `rounds: 1-5`. Use for decisions with genuine trade-offs. ```yaml - type: "debate" id: "build_vs_buy" debaters: - { agent: one-agent, position: "build in-house" } - { agent: one-agent, position: "buy off the shelf" } judge: { agent: one-agent } rounds: 2 ``` # plungeai-in-bolt Source: https://www.plungeai.com/docs/skills/plungeai-in-bolt Connect Bolt.new to PlungeAI (Ocean Studio) over MCP — the Connectors → Custom MCP server form, the all-tools/all-projects toggle behavior, keeping the shipped app's key server-side, and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in Bolt.new and says connect / install / set up PlungeAI, mentions Custom MCP server, or a deployed Bolt app 401s against PlungeAI. For another builder use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-bolt.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-bolt/SKILL.md) Bolt is a hosted builder: there is no local config file and no JSON entry anywhere — the connector form is the only surface, and the key lives only in its credential. ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Connect (once) From the Bolt homepage chatbox: click the **+** icon → **Connectors** → **Manage connectors** → **Custom MCP server**, then fill: | Field | Value | |---|---| | Name | `PlungeAI` | | URL | `https://mcp.plungeai.com/v1` | | Transport type | `HTTP` (use `SSE` only if a server's docs say so — PlungeAI's is HTTP) | | Authentication | select **API key** and enter the `ozk_` key (or **MCP OAuth** — not available for PlungeAI yet) | Bolt does not document which header the API-key field sends; the PlungeAI server accepts both `X-API-Key: ozk_YOUR_KEY` and `Authorization: Bearer ozk_YOUR_KEY`, so the raw key works — confirm with `plungeai_whoami` (Verify below). ## Verify 1. Builder chat: "use plungeai_whoami to confirm my identity" → an identity card. 2. Generated app (server side): `curl -H "Authorization: Bearer ozk_YOUR_KEY" https://api.plungeai.com/v1/agents` → HTTP 200 JSON. 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. ## Quirks — the toggles that bite - **Auto-enable for all projects** controls whether the connector turns on automatically in each new project — existing projects enable it from the same Connectors menu. - Adding a connector turns on all its tools by default, and the tool list is global — you can't turn tools on/off per project. Trim the tool list once, globally, if the builder gets tool-choice-noisy. - Tools missing mid-session → Manage connectors: check the connector is enabled for this project and its tool toggles are on, then retry. - The connector credential authenticates the BUILDER only. The shipped app needs its own wiring: a server function holding `PLUNGEAI_API_KEY` as an env var that proxies the One API. An app that "worked in preview" but 401s when deployed usually shipped without the env var set. The key must never appear in the client bundle or a browser `fetch`. - Long CNL workflows can exceed a builder-chat step budget — prefer async mode (`plungeai_execute_workflow` with `mode: "async"` + `plungeai_get_workflow_status`). ## What the shipped app needs to know (One API 2.3.0) The app's server function is the only caller of the One API: it reads `PLUNGEAI_API_KEY` from its env and sets the Authorization header itself (no connector attaches auth for the shipped app). Give the builder this block in the build prompt so it implements real routes instead of guessing. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Where next - Writing the server-side proxy that calls `https://api.plungeai.com`: **plungeai-api-setup**. - Operating the `plungeai_*` tools in chat: **plungeai-mcp-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-claude-ai Source: https://www.plungeai.com/docs/skills/plungeai-in-claude-ai Connect claude.ai web or Claude Desktop to PlungeAI (Ocean Studio) — Desktop live tools via the mcp-remote bridge in claude_desktop_config.json (OAuth isn't available yet, hence the bridge), claude.ai web via skill.zip upload, and a plungeai_whoami + plungeai_list_agents verify for Desktop. Use when the user is on claude.ai or Claude Desktop and says connect / install / set up PlungeAI, mentions claude_desktop_config.json, Customize → Skills, or a PlungeAI tool call there is failing. For the Claude Code CLI use plungeai-in-claude-code; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-claude-ai.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-claude-ai/SKILL.md) Two different surfaces, two different capabilities: | Surface | What it gets | |---|---| | **Claude Desktop** | live `plungeai_*` MCP tools, via a bridge (below) | | **claude.ai (web)** | a PlungeAI *skill* upload — Claude produces paste-ready CNL workflows for Ocean Studio, not live tool calls (claude.ai has no custom-connector bearer-header field yet) | ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. - Desktop only: Node.js installed (the bridge runs via `npx`). ## Connect (once) — Claude Desktop **Why a bridge:** Settings → Connectors → Add custom connector authenticates through OAuth only (no bearer-header field), and PlungeAI OAuth hasn't shipped. Until it does, Desktop connects through the `mcp-remote` stdio bridge. Settings → Developer → **Edit Config** opens the file — macOS `~/Library/Application Support/Claude/claude_desktop_config.json`, Windows `%APPDATA%\Claude\claude_desktop_config.json`. Merge: { "mcpServers": { "plungeai": { "command": "npx", "args": ["-y", "mcp-remote", "`https://mcp.plungeai.com/v1`", "--header", "Authorization: Bearer ozk_YOUR_KEY"] } } } Quit Claude Desktop completely and relaunch — the config is only read at start. ## Connect (once) — claude.ai web (skill only) Needs code execution enabled first: **Settings → Capabilities** → turn on **Code execution and file creation** (a prerequisite for any custom skill upload, not PlungeAI-specific). Then **Customize → Skills → + → Upload a skill** — upload https://mcp.plungeai.com/skill.zip. This teaches Claude PlungeAI conventions; it does not give claude.ai a live MCP connection. ## Verify (Claude Desktop) 1. Ask Claude: "use plungeai_whoami to confirm my identity" → an identity card (user id, auth tier, key label, rate window, server). 2. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. claude.ai web has no live tool call to verify — a successfully uploaded skill is the only signal; ask Claude to draft a PlungeAI CNL workflow to confirm it loaded. ## The tool map (Claude Desktop) Desktop gets the live tools; claude.ai web (skill upload) has none. This is what the 22 tools take, so Claude can plan a call before making it: ```text PlungeAI MCP 2.7.0. Server https://mcp.plungeai.com/v1 (Streamable HTTP), Authorization: Bearer ozk_YOUR_KEY. 22 tools; tools/list is authoritative (a fenced key sees fewer). Pass user_request (the user's own words, verbatim) on every call. Output is final markdown: relay it verbatim. format:"json" returns machine-readable output. FIND: plungeai_list_agents {search, category, agent_id, kind: agents|personas|experts|skills|models|workflows|connectors} = the live registry; ids come only from here. plungeai_get_tool_contract {agent_id} = schema, operations, credential status. plungeai_whoami = identity, tier, key label, rate window. RUN: plungeai_execute_agent {agent, prompt} (always sync). plungeai_execute_tool {agent_id, operation, params, mode}. plungeai_run_mission {goal, allowed_tools, max_iterations} (async by default). plungeai_execute_workflow {workflow_id | workflow_yaml, input, inputs, mode: sync|async}. RUNS: plungeai_get_workflow_status {execution_id} polls (and carries continuation). plungeai_get_result {execution_id, task_id?}. plungeai_followup {execution_id, prompt}. plungeai_continue {execution_id, approve | message}. plungeai_executions {action: list|get|output|conversation|delete|cancel|steer|files; message for steer}. WORKFLOWS: plungeai_list_workflows {kind: workflow|agent|bot|campaign, folder, search}. plungeai_build_workflow {goal | workflow_id + instruction}. plungeai_templates {action: list|get|use}. plungeai_workflow {action: create|get|update|delete|list_versions|get_version|save_version|restore_version|a2a_door|permissions_get|permissions_set|files|file|inbox|bindings|bind|unbind|groups|group_set|group_delete|group_thread; kind}. SCHEDULES: plungeai_schedule {action: stats|list|get|create|update|pause|resume|delete|run_now|runs|wake|webhook_mint|webhook_list|webhook_delete|email_mint|email_list|email_delete|hooks_mint|hooks_list|hooks_delete}; create takes job_type agent|query|workflow|heartbeat, target, schedule (cron, or "@once" with run_at = ISO-8601 with a UTC offset). MEMORY: plungeai_memory {action: recall|remember|search_runs|get_run}. plungeai_learn {action: learn|list|forget} (learn defaults to async). plungeai_chat {action: send|list_sessions|history|new}. OUTCOMES: status ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error, with remediation.actions. A paused run ends with a pause block: relay it verbatim, call plungeai_continue only after the user decides (approve:true for an explicit yes, message for anything else). Never approve for the user; never retry the identical call blind. ``` ## Quirks - **Windows:** Desktop does not escape spaces inside `args` — if the bridge 401s only on Windows, replace the header arg with `"--header", "Authorization:${AUTH_HEADER}"` plus `"env": { "AUTH_HEADER": "Bearer ozk_YOUR_KEY" }` in the same entry. - Logs: `~/Library/Logs/Claude/mcp*.log` (macOS) · `%APPDATA%\Claude\logs` (Windows) — check these before re-editing the config. - The bridge is an `npx` process — a cold start can take a few seconds; if tools "never appear," wait, then check the logs above. - Long CNL workflow runs: prefer `mode: "async"` + `plungeai_get_workflow_status` polling — Desktop caps a single synchronous tool call. - Money-verb operations (payments, sends) still stop: a gated call pauses with a structured `needs_approval` outcome — relay it to the user, then `plungeai_continue`; never retry blind. - 401 through the bridge but the key looks right → whitespace or a missing `ozk_` prefix; re-paste it into the config. ## Where next - The Claude Code CLI instead of Desktop/web: **plungeai-in-claude-code**. - Operating the `plungeai_*` tools once connected: **plungeai-mcp-setup**. - Writing code that calls `https://api.plungeai.com` directly: **plungeai-api-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-claude-code Source: https://www.plungeai.com/docs/skills/plungeai-in-claude-code Connect the Claude Code CLI to PlungeAI (Ocean Studio) over MCP — 'claude mcp add' one-liner, project-scoped .mcp.json with an env-var key, the skills/plugin install path, and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in Claude Code and says connect / install / set up PlungeAI, add an MCP server, mentions claude mcp add or ozk_, or a PlungeAI tool call in Claude Code is failing. For claude.ai web or Claude Desktop use plungeai-in-claude-ai; for another editor use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-claude-code.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-claude-code/SKILL.md) Claude Code talks to PlungeAI over the Streamable HTTP MCP server at `https://mcp.plungeai.com/v1`. One `ozk_` bearer key authenticates it (and the One API, if generated code calls PlungeAI directly). ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. - Claude Code CLI installed and on PATH (`claude --version`). ## Connect (once) ### Terminal one-liner (simplest) claude mcp add --transport http plungeai `https://mcp.plungeai.com/v1` \ --header "Authorization: Bearer ozk_YOUR_KEY" --scope user - Default scope is `local` — this project only, stored in `~/.claude.json`. `--scope user` makes the server available in every project (same file). - **Never `--scope project` with a literal key** — that writes the key into `.mcp.json` inside the repo, which gets committed. ### Project-scoped `.mcp.json` (committed-safe, env-var key) Use this when the team should share the server definition in the repo without anyone's literal key landing in git: { "mcpServers": { "plungeai": { "type": "http", "url": "`https://mcp.plungeai.com/v1`", "headers": { "Authorization": "Bearer ${PLUNGEAI_API_KEY}" } } } } Export `PLUNGEAI_API_KEY` in each developer's own shell before launching Claude Code — the file itself never holds a literal key, so it's safe to commit. ## Verify 1. `claude mcp list` (or `/mcp` inside a session) shows `plungeai` connected. 2. Ask Claude: "use plungeai_whoami to confirm my identity" → an identity card (user id, auth tier, key label, rate window, server). 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog — proves the catalog is reachable, not just auth. ## Quirks - `tools/list` is authoritative: a key can be fenced to a subset of `plungeai_*` tools — seeing fewer tools than a colleague is policy, not a bug. - Money-verb operations (payments, sends) still stop: a gated call pauses with a structured `needs_approval` outcome (relay it, then `plungeai_continue` once the user decides) regardless of any auto-approve setting — never retry blind, never approve on your own. - Long CNL workflow runs: prefer `mode: "async"` + `plungeai_get_workflow_status` polling over one long synchronous call. - Skills for the agent — teach PlungeAI conventions (discovery-first, CNL authoring) as an installable skill directory: - **Public repo `PlungeAI/plungeai-agent-skills` — once published**: `npx skills add PlungeAI/plungeai-agent-skills` (installs into `.agents/skills/` or `.claude/skills/`, `-g` for the user-level home `~/.claude/skills/`), or `claude plugin marketplace add PlungeAI/plungeai-agent-skills` then `claude plugin install plungeai@plungeai`. - **Working today:** download https://mcp.plungeai.com/skill.zip and unzip into `.claude/skills/` (or `~/.claude/skills/`). - 401 (`No valid authentication provided...`; a `WWW-Authenticate: Bearer error="invalid_token"` header means a key arrived and was rejected) → key typo'd, pasted with whitespace, or the header never made it into `~/.claude.json` / `.mcp.json` — re-check the header field, not the key value. - Connected but no `plungeai_*` tools listed → restart the session after any config edit before debugging further. ## What the agent should know (One API 2.3.0) The `plungeai_*` tools operate PlungeAI from chat; the block below is for the code the agent writes against `https://api.plungeai.com`. This skill already carries it. To make it always-on for a project, paste it into `CLAUDE.md` — that file is committed, so never put the key in it. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Slash commands (`/ocean-*`) Installing the plugin (`/plugin marketplace add PlungeAI/plungeai-agent-skills`, `/plugin install plungeai@plungeai`) also adds one command per PlungeAI job; each runs `ocean-cli` for you (install and log in first: **plungeai-cli-setup**). `/ocean` lists them all: - Workflows: `/ocean-create <goal>`, `/ocean-refine <id> <change>`, `/ocean-edit <id>`, `/ocean-show [id|name|bots]`, `/ocean-run <id|file> [-i input]`, `/ocean-versions <id> [save|restore <v>|undo]`, `/ocean-delete <id>`. - Runs: `/ocean-runs`, `/ocean-results [id]`, `/ocean-followup <id> <question>`, `/ocean-thread <id>`, `/ocean-steer <id> <text>`, `/ocean-export <id>`. - Schedules and missions: `/ocean-schedule`, `/ocean-schedule-once <workflow-id> <time>`, `/ocean-mission <goal>`. - Bots: `/ocean-bot-permissions <bot-id>`, `/ocean-bot-files <bot-id>`, `/ocean-bot-groups`. - Platform: `/ocean-agent`, `/ocean-search <objective>`, `/ocean-memory`, `/ocean-templates`, `/ocean-learn`, `/ocean-sessions`, `/ocean-local`, `/ocean-whoami`, `/ocean-doctor`. ## Where next - Operating the `plungeai_*` tools once connected: **plungeai-mcp-setup**. - The `ocean-cli` command line the slash commands use: **plungeai-cli-setup** (install, login), **ocean-cli** (every verb). - Writing code that calls `https://api.plungeai.com` directly: **plungeai-api-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - CNL workflow YAML authoring: **plungeai-workflows**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-codex Source: https://www.plungeai.com/docs/skills/plungeai-in-codex Connect OpenAI Codex CLI to PlungeAI (Ocean Studio) over MCP — native streamable-HTTP in ~/.codex/config.toml (env-var or static bearer token), the codex mcp add terminal command, the mcp-remote bridge fallback, and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in Codex CLI (or asks about ChatGPT + PlungeAI) and says connect / install / set up PlungeAI, mentions config.toml or mcp_servers, or plungeai_* tools never appear. ChatGPT web has no PlungeAI connector yet (OAuth pending) — Codex CLI is the OpenAI-ecosystem door. For another editor use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-codex.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-codex/SKILL.md) Codex CLI talks to PlungeAI over the Streamable HTTP MCP server at `https://mcp.plungeai.com/v1`. One `ozk_` bearer key authenticates it (and the One API, if generated code calls PlungeAI directly). This is also the OpenAI-ecosystem door today: ChatGPT web connectors require OAuth (PlungeAI OAuth in progress), so ChatGPT users connect through Codex CLI instead. ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Connect (once) — native streamable HTTP (primary) Codex supports remote streamable-HTTP servers natively. In `~/.codex/config.toml` (user-level TOML — the right home for anything key-related): [mcp_servers.plungeai] url = "`https://mcp.plungeai.com/v1`" bearer_token_env_var = "PLUNGEAI_API_KEY" Export `PLUNGEAI_API_KEY` in the shell profile; the config file itself never holds the key, so this pattern is safe even if the file is shared. A static header works too — user-level file only, never committed: http_headers = { Authorization = "Bearer ozk_YOUR_KEY" } Or from the terminal: codex mcp add plungeai --url `https://mcp.plungeai.com/v1` --bearer-token-env-var PLUNGEAI_API_KEY codex mcp list ## Fallback — mcp-remote bridge (only if the build lacks native `url`) [mcp_servers.plungeai] command = "npx" args = ["-y", "mcp-remote", "`https://mcp.plungeai.com/v1`", "--header", "Authorization: Bearer ozk_YOUR_KEY"] startup_timeout_sec = 30 `startup_timeout_sec` defaults to **10 s** — an npx cold start can exceed it, and then the tools "never appear" with no useful error; set 30. If the bridge 401s, the client is mangling spaces inside `args` — switch to the native form above rather than fighting the quoting. ## Verify 1. `codex mcp list` shows `plungeai` (or `/mcp` in the TUI shows it connected). 2. Ask Codex: "use plungeai_whoami to confirm my identity" → an identity card. 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. ## Quirks - `config.toml` is TOML, not JSON — no braces or trailing commas; the table header is exactly `[mcp_servers.plungeai]` (underscore, not dash). - `tool_timeout_sec` defaults to 60 s — long CNL workflows should run in async mode (`plungeai_execute_workflow` with `mode: "async"` + `plungeai_get_workflow_status`) rather than one long synchronous call. - `/mcp` inside the Codex TUI shows active MCP servers and their status; `codex mcp get plungeai` / `codex mcp remove plungeai` manage the entry. - Codex approval modes gate tool calls; in full-auto the money-verb PlungeAI operations still pause with a structured `needs_approval` outcome — relay it, then `plungeai_continue`. - `enabled = false` disables the server without deleting the entry. - ChatGPT web: no PlungeAI connector yet — OAuth is in progress. Point ChatGPT users at Codex CLI in the meantime. ## What the agent should know (One API 2.3.0) The `plungeai_*` tools operate PlungeAI from chat; the block below is for the code the agent writes against `https://api.plungeai.com`. This skill already carries it. To make it always-on for a project, paste it into `AGENTS.md` — that file is committed, so never put the key in it. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Where next - Operating the `plungeai_*` tools once connected: **plungeai-mcp-setup**. - Writing code that calls `https://api.plungeai.com` directly: **plungeai-api-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-cursor Source: https://www.plungeai.com/docs/skills/plungeai-in-cursor Connect Cursor to PlungeAI (Ocean Studio) over MCP — the one-click install-page deeplink or a ~/.cursor/mcp.json entry, Agent/Plan-mode tool behavior, the ~40-tool cap, and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in Cursor and says connect / install / set up PlungeAI, mentions mcp.json or the install page, asks whether PlungeAI works in Cursor, or plungeai_* tools aren't showing up. For another editor use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-cursor.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-cursor/SKILL.md) Cursor talks to PlungeAI over the Streamable HTTP MCP server at `https://mcp.plungeai.com/v1`. One `ozk_` bearer key authenticates it (and the One API, if generated code calls PlungeAI directly). ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Connect (once) ### One click 1. Open https://mcp.plungeai.com/install, **type the `ozk_` key into the box at the top**, then click **Add to Cursor**. Cursor prompts to install the server — confirm. 2. Cursor never asks for a header: clicked without the key, the server installs auth-less (401, red status) and the `headers` block must be added by hand afterwards (below). ### By hand Merge into `~/.cursor/mcp.json` (user-level — the right home for the key): { "mcpServers": { "plungeai": { "url": "`https://mcp.plungeai.com/v1`", "headers": { "Authorization": "Bearer ozk_YOUR_KEY" } } } } A project-scoped `.cursor/mcp.json` gets committed — there, reference an env var instead: `"Authorization": "Bearer ${env:PLUNGEAI_API_KEY}"` (Cursor interpolates `${env:…}` in `command`, `args`, `env`, `url`, and `headers`). ## Verify 1. Settings → MCP → `plungeai` shows a green dot with tools listed. 2. In Agent chat: "use plungeai_whoami to confirm my identity" → an identity card. 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. ## Quirks - MCP tools fire in **Agent** and **Plan** modes; **Ask** mode is read-only and won't call them. - Tool calls run behind per-call approval by default. Money-verb PlungeAI operations still pause with a structured `needs_approval` outcome even under auto-run — relay it, then `plungeai_continue`. - Tool cap: users report a limit of roughly 40 MCP tools sent to the model across all servers (unverified, not in Cursor's current docs). If `plungeai_*` tools are missing, disable unused servers or toggle off unneeded tools, then reopen the chat. - After editing `mcp.json`, toggle the server off/on in Settings → MCP (or restart Cursor) before debugging anything else. - Red status / "No tools found" right after install → check auth first: the `headers` object must sit *inside* the `plungeai` entry (not beside it) and the key must be intact. ## What the agent should know (One API 2.3.0) The `plungeai_*` tools operate PlungeAI from chat; the block below is for the code the agent writes against `https://api.plungeai.com`. This skill already carries it. To make it always-on for a project, paste it into `.cursor/rules/plungeai.mdc` — that file is committed, so never put the key in it. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Where next - Operating the `plungeai_*` tools once connected: **plungeai-mcp-setup**. - Writing code that calls `https://api.plungeai.com` directly: **plungeai-api-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-gemini-cli Source: https://www.plungeai.com/docs/skills/plungeai-in-gemini-cli Connect Gemini CLI to PlungeAI (Ocean Studio) over MCP — a ~/.gemini/settings.json entry using httpUrl (not url, which is SSE-only and 405s), the mcp-remote bridge fallback, /mcp reload after edits, and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in Gemini CLI and says connect / install / set up PlungeAI, mentions settings.json or httpUrl, or plungeai_* tools come up empty. For another editor use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-gemini-cli.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-gemini-cli/SKILL.md) Gemini CLI talks to PlungeAI over the Streamable HTTP MCP server at `https://mcp.plungeai.com/v1`. One `ozk_` bearer key authenticates it (and the One API, if generated code calls PlungeAI directly). ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Connect (once) Merge into `~/.gemini/settings.json` (user-level — the right home for the key): { "mcpServers": { "plungeai": { "httpUrl": "`https://mcp.plungeai.com/v1`", "headers": { "Authorization": "Bearer ozk_YOUR_KEY" } } } } **The key name is `httpUrl`** — Gemini CLI reserves `url` for SSE servers, and the PlungeAI endpoint rejects SSE-style GETs with an immediate 405, so a `url` entry fails to connect. This is the #1 misconfiguration on this tool. If the build lacks `httpUrl`, bridge with mcp-remote instead: { "mcpServers": { "plungeai": { "command": "npx", "args": ["-y", "mcp-remote", "`https://mcp.plungeai.com/v1`", "--header", "Authorization: Bearer ozk_YOUR_KEY"] } } } A project-scoped `.gemini/settings.json` gets committed — keep the literal key in the user-level file only. ## Verify 1. `/mcp` inside the CLI lists `plungeai` with its tools. 2. Ask Gemini: "use plungeai_whoami to confirm my identity" → an identity card. 3. Transport-independent check: `curl -H "Authorization: Bearer ozk_YOUR_KEY" https://api.plungeai.com/v1/agents` → HTTP 200 JSON. Then "use plungeai_list_agents to search 'web search'" from inside the CLI → live results from the active agent catalog. ## Quirks - `/mcp reload` re-connects all MCP servers and re-discovers tools after a settings edit — `/mcp refresh` exists on some builds but has had reload bugs upstream; if either doesn't pick up the change, restart the CLI. - Per-tool confirmation is the default; `"trust": true` on the server entry skips it. Money-verb PlungeAI operations still pause with a structured `needs_approval` outcome regardless — relay it, then `plungeai_continue`. - A previously working setup whose tool list comes up empty usually means the key was revoked/expired (the handshake now 401s) — re-check with the curl line above before touching the config. ## What the agent should know (One API 2.3.0) The `plungeai_*` tools operate PlungeAI from chat; the block below is for the code the agent writes against `https://api.plungeai.com`. This skill already carries it. To make it always-on for a project, paste it into `GEMINI.md` — that file is committed, so never put the key in it. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Where next - Operating the `plungeai_*` tools once connected: **plungeai-mcp-setup**. - Writing code that calls `https://api.plungeai.com` directly: **plungeai-api-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-lovable Source: https://www.plungeai.com/docs/skills/plungeai-in-lovable Connect Lovable to PlungeAI (Ocean Studio) — a personal chat connector (any plan) so Lovable's chat can operate PlungeAI via MCP while building, and a workspace-admin app connector so shipped apps call the One API in production with attached auth, plus a plungeai_whoami + plungeai_list_agents verify. Use when the user is in Lovable and says connect / install / set up PlungeAI, mentions Connectors or Custom MCP, or a Lovable-built app can't reach PlungeAI. For another builder use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-lovable.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-lovable/SKILL.md) Two connectors, two purposes: a **chat connector** (any plan) so Lovable's chat can operate PlungeAI while building, and an **app connector** (workspace admin) so the apps users ship call the One API in production. ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Path A — chat connector (any plan, 2 minutes) **Connectors → + → MCP server**: Server name `PlungeAI`, Server URL `https://mcp.plungeai.com/v1`, Authentication → bearer token / API key → paste the `ozk_` key. Lovable's chat now has the 20 `plungeai_*` tools while building (tool map: **plungeai-mcp-setup**). - Chat connectors are **personal**: others in the workspace connect the same server themselves, each with their own key. - They are **build-time only** — never part of the published app. That is exactly why Path B exists. - Workspace admins can switch custom MCP servers off for everyone: **Connectors → Admin settings → Chat connectors → "Custom MCP"**. A non-admin who can't find "MCP server" in the picker is hitting this toggle. ## Path B — app connector (workspace admin) Creates a reusable custom connector: workspace members connect it to their apps, each pasting their own key; Lovable then attaches auth to every One API call the generated app makes. ### 1. Create the connector (Connectors → + → Custom connector) | Form field | Value | | --- | --- | | Display name | PlungeAI | | Category | Development | | Documentation URL | https://api.plungeai.com/docs | ### 2. Authentication | Field | Value | | --- | --- | | Authentication method | Bearer token | | API base URL | `https://api.plungeai.com` | | Test request → Method | GET | | Test request → Path | /v1/agents | Users paste their own `ozk_` key when they connect an app; Lovable verifies it against the verification path (green check = valid key). ### 3. Agent knowledge (attach to the connector) Paste this whole block into the connector's agent knowledge. It is the One API as deployed (2.3.0), so the generated app can implement saved workflows, bots, executions, schedules, triggers, memory, skills, templates and connections, not only "run an agent". The connector's auth layer already attaches the key: generated code never adds its own Authorization header. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Verify 1. Chat: "use plungeai_whoami to confirm my identity" → an identity card. 2. App connector: connecting an app runs Lovable's verification (`GET /v1/agents`) → green check. 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. ## Quirks - Two different auth surfaces: the chat connector's key powers the BUILDER; each app's key is pasted by its owner when connecting the app connector. Never hardcode any key in the app itself. - If generated code contains an `Authorization` header, the knowledge wasn't attached to the connector, or the connector step was skipped — re-check step 3; the connector's own auth layer is the only place credentials belong. - The app connector is workspace-admin gated; non-admins use Path A for building and hand the admin this file for Path B. ## Where next - Route-by-route API manual (workflows, bots, executions, schedules, triggers, connections, discovery, traces): **plungeai-api-setup**. - Operating the `plungeai_*` tools in chat: **plungeai-mcp-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-opencode Source: https://www.plungeai.com/docs/skills/plungeai-in-opencode Connect OpenCode to PlungeAI (Ocean Studio) over MCP — an opencode.json mcp.plungeai entry with type: remote, {env:VAR} variable substitution for a committed project config, and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in OpenCode and says connect / install / set up PlungeAI, mentions opencode.json or the mcp key, or plungeai_* tools aren't responding. For another editor use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-opencode.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-opencode/SKILL.md) OpenCode talks to PlungeAI over the Streamable HTTP MCP server at `https://mcp.plungeai.com/v1`. One `ozk_` bearer key authenticates it (and the One API, if generated code calls PlungeAI directly). ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Connect (once) Merge into `~/.config/opencode/opencode.json` (user-level — the right home for the literal key) or the project's `opencode.json`: { "mcp": { "plungeai": { "type": "remote", "url": "`https://mcp.plungeai.com/v1`", "headers": { "Authorization": "Bearer ozk_YOUR_KEY" } } } } Note the shape: the top-level key is `mcp` (not `mcpServers`), and remote servers need `"type": "remote"`. For a committed project `opencode.json`, use OpenCode's variable substitution so the literal key never lands in the repo: "headers": { "Authorization": "Bearer {env:PLUNGEAI_API_KEY}" } (`{file:path}` substitution also exists for key files.) ## Verify 1. Ask OpenCode: "use plungeai_whoami to confirm my identity" → an identity card. 2. Transport-independent check: `curl -H "Authorization: Bearer ozk_YOUR_KEY" https://api.plungeai.com/v1/agents` → HTTP 200 JSON. 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. ## Quirks - Project config merges over the user config — if tools behave oddly, check whether a committed `opencode.json` (or `.jsonc`) redefines `mcp.plungeai`, e.g. with `"enabled": false`. - `{env:VAR}` resolves at startup: export `PLUNGEAI_API_KEY` before launching OpenCode, or the header goes out empty → 401. - MCP servers connect at startup — restart the OpenCode session after any config edit before debugging further. - Money-verb PlungeAI operations still pause with a structured `needs_approval` outcome (relay it, then `plungeai_continue`) regardless of any approval setting. ## What the agent should know (One API 2.3.0) The `plungeai_*` tools operate PlungeAI from chat; the block below is for the code the agent writes against `https://api.plungeai.com`. This skill already carries it. To make it always-on for a project, paste it into `AGENTS.md` (OpenCode reads it) — that file is committed, so never put the key in it. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Where next - Operating the `plungeai_*` tools once connected: **plungeai-mcp-setup**. - Writing code that calls `https://api.plungeai.com` directly: **plungeai-api-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-replit Source: https://www.plungeai.com/docs/skills/plungeai-in-replit Connect Replit Agent to PlungeAI (Ocean Studio) over MCP — the one-click install-page link, the Integrations → MCP Servers form, keeping the Agent's MCP key separate from a deployed app's Replit Secret, and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in Replit and says connect / install / set up PlungeAI, mentions MCP Servers or Replit Secrets, or an app built in Replit 401s against PlungeAI. For another editor use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-replit.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-replit/SKILL.md) Replit talks to PlungeAI over the Streamable HTTP MCP server at `https://mcp.plungeai.com/v1` — for the Agent while building. Apps the Agent ships call the One API (`https://api.plungeai.com`) directly with their own key in Replit Secrets. ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Connect (once) ### One click 1. Open https://mcp.plungeai.com/install, **type the `ozk_` key into the box at the top**, then click **Add to Replit** — the integration imports with its Authorization header. Clicked without the key, add the header afterwards under the integration's **Advanced settings**. ### By hand Integrations → **MCP Servers** → **+ Add MCP server**: **Display name** `PlungeAI`, **Server URL** `https://mcp.plungeai.com/v1`, then **Advanced settings** → custom header `Authorization` = `Bearer ozk_YOUR_KEY`. The integration is account-level: the Agent gets the `plungeai_*` tools across all your projects, not just the current Repl. ## Verify 1. Ask the Agent: "use plungeai_whoami to confirm my identity" → an identity card. 2. From the Repl shell: `curl -H "Authorization: Bearer $PLUNGEAI_API_KEY" https://api.plungeai.com/v1/agents` → HTTP 200 JSON (proves a Repl Secret is set and valid). 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. ## Quirks - Two key locations — never mixed: the MCP integration's header (for the Agent) and **Replit Secrets** as `PLUNGEAI_API_KEY` (for the Repl's own code). Never in source files, `.replit`, or `replit.nix`. - A workspace app that 401s after deploy → check that the deployment's secrets carry `PLUNGEAI_API_KEY` too. - Tool approval: a tool that requires confirmation prompts before it runs. Money-verb PlungeAI operations still pause with a structured `needs_approval` outcome regardless — relay it, then `plungeai_continue`. - Standard split when the Agent is asked to "build an app that uses PlungeAI": it uses MCP to discover and test the right agent, then writes app code that calls the One API with the key from Replit Secrets. ## What the Agent should know when it writes app code (One API 2.3.0) The MCP integration serves the Agent while it builds; the Repl's own code calls the One API with `PLUNGEAI_API_KEY` from Replit Secrets. Put this block in `replit.md` (Agent reads it on every request; the file is part of the project, so never the key) or paste it into the prompt. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Where next - Operating the `plungeai_*` tools once connected: **plungeai-mcp-setup**. - Writing app code that calls `https://api.plungeai.com` directly: **plungeai-api-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-v0 Source: https://www.plungeai.com/docs/skills/plungeai-in-v0 Connect v0 (Vercel) to PlungeAI (Ocean Studio) over MCP — the + menu → MCPs form, why the generated Next.js app must call PlungeAI through a server Route Handler (v0 apps can't call MCP directly), and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in v0 and says connect / install / set up PlungeAI, mentions the MCPs panel, or a generated v0 app 401s against PlungeAI. For another builder use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-v0.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-v0/SKILL.md) v0 is a hosted builder: no local config file and no JSON entry anywhere — the MCP form is the only surface, and the key lives only in that entry. ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Connect (once) Open the **+** menu in the prompt form → **MCPs** → add your own server (the project settings page offers the same via **Add MCP**): | Field | Value | |---|---| | Name | `PlungeAI` | | URL | `https://mcp.plungeai.com/v1` | | Authentication | **Bearer Token** → paste the `ozk_` key itself (v0 adds the `Bearer` prefix); or **Custom Headers** → `Authorization` = `Bearer ozk_YOUR_KEY`. If one form 401s, try the other. (`No Auth` and `OAuth` don't apply — PlungeAI OAuth hasn't shipped.) | Then pick the server's **permission mode**: **Disabled**, **Ask for Approval (Manual)**, or **Always Run (Auto)**. Manual is the sane default; money-verb PlungeAI operations still pause with a structured `needs_approval` outcome even under Auto — relay it, then `plungeai_continue`. ## The generated app cannot call MCP v0's generated code cannot use the MCP tools directly — pair the MCP with **Environment Variables**. So the split is: the builder chat operates PlungeAI over MCP; the generated Next.js app calls the One API server-side only, e.g. a Route Handler: // app/api/plungeai/route.ts export async function POST(req: Request) { const { input, agentId } = await req.json() const r = await fetch( `https://api.plungeai.com/v1/agents/${agentId}/execute`, { method: "POST", headers: { Authorization: `Bearer ${process.env.PLUNGEAI_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ input, sync: true }), }, ) return Response.json(await r.json()) } Key in a Vercel env var (`PLUNGEAI_API_KEY`, every environment that needs it), never in client components, never prefixed `NEXT_PUBLIC_`. ## What to tell v0 about the One API (2.3.0) When the app needs more than one agent call, paste this block into the prompt that asks v0 for its Route Handlers. Every route stays server-side and sets `Authorization: Bearer ${process.env.PLUNGEAI_API_KEY}` itself. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Verify 1. Builder chat: "use plungeai_whoami to confirm my identity" → an identity card. 2. Generated app (server side): `curl -H "Authorization: Bearer ozk_YOUR_KEY" https://api.plungeai.com/v1/agents` → HTTP 200 JSON. 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. ## Quirks - v0 previews run without the project's env vars until linked/deployed — a preview 401 from the app does not mean the integration is wrong; verify with the curl check above, then set the env var on the Vercel project. - If generation puts the fetch in a `"use client"` component, regenerate with: "PlungeAI calls go through a Route Handler; the key stays server-side." - Long CNL workflows: prefer async mode (`plungeai_execute_workflow` with `mode: "async"` + `plungeai_get_workflow_status`) over one long synchronous builder-chat call. ## Where next - Writing the Route Handler / server code that calls `https://api.plungeai.com`: **plungeai-api-setup**. - Operating the `plungeai_*` tools in chat: **plungeai-mcp-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-vscode Source: https://www.plungeai.com/docs/skills/plungeai-in-vscode Connect VS Code (GitHub Copilot agent mode) to PlungeAI (Ocean Studio) over MCP — the code --add-mcp CLI command, a committed-safe .vscode/mcp.json with an input-prompted key, the 128-tools-per-request cap, and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in VS Code/Copilot and says connect / install / set up PlungeAI, mentions mcp.json or code --add-mcp, or plungeai_* tools aren't appearing in agent mode. For another editor use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-vscode.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-vscode/SKILL.md) VS Code talks to PlungeAI over the Streamable HTTP MCP server at `https://mcp.plungeai.com/v1`. One `ozk_` bearer key authenticates it (and the One API, if generated code calls PlungeAI directly). ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Connect (once) ### From a terminal (simplest working path) code --add-mcp '{"name":"plungeai","type":"http","url":"`https://mcp.plungeai.com/v1`","headers":{"Authorization":"Bearer ozk_YOUR_KEY"}}' (PowerShell/cmd: double-quote the JSON and escape inner quotes with `\"`.) ### By hand — the committed-safe pattern `.vscode/mcp.json` in the workspace. The `inputs` block makes VS Code prompt for the key once and store it encrypted — the file never holds the key, so it is safe to commit: { "inputs": [ { "id": "plungeai-key", "type": "promptString", "password": true, "description": "PlungeAI ozk_ key" } ], "servers": { "plungeai": { "type": "http", "url": "`https://mcp.plungeai.com/v1`", "headers": { "Authorization": "Bearer ${input:plungeai-key}" } } } } Machine-wide instead: **MCP: Open User Configuration** (the `mcp.json` in your user profile), or **MCP: Add Server** → choose the user scope — same fields, outside any repo. The install page's "Add to VS Code" button emits one URL-encoded JSON object (`vscode:mcp/install?{"name":"plungeai","type":"http","url":…}`) — matching VS Code's documented deeplink shape. If it ever fails to parse, `code --add-mcp` or the file above works unconditionally. ## Verify 1. **MCP: List Servers** shows `plungeai` running; the Tools picker lists `plungeai_*`. 2. In Copilot agent chat: "use plungeai_whoami to confirm my identity" → an identity card. 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. ## Quirks - MCP tools run in **agent mode** (Chat view → mode picker → Agent). The `plungeai_*` tools appear in the chat Tools picker once the server starts. - **128 tools max per chat request**, across all sources. Over the limit: deselect other servers/tools in the Tools picker, or rely on virtual tools — `github.copilot.chat.virtualTools.threshold` auto-groups tools beyond the threshold and lets the model expand a group on demand. - Mistyped key: **MCP: List Servers** → `plungeai` → **Show Output** shows the 401. Restart the server from the same menu; if it doesn't re-prompt for the stored input, remove and re-add the server. - Locked-down setups: `chat.mcp.access` governs which MCP servers can be used; org policies (`ChatAllowedMcpServers` / `ChatDeniedMcpServers` / `ChatAllowManagedMcpServersOnly`) allow- or deny-list servers — a missing MCP section in settings usually means one of these. - Trust: VS Code asks you to confirm you trust the server before first start — except when it's started directly from `mcp.json`. - Money-verb PlungeAI operations still pause with a structured `needs_approval` outcome (relay it, then `plungeai_continue`) regardless of any trust/confirmation setting. ## What the agent should know (One API 2.3.0) The `plungeai_*` tools operate PlungeAI from chat; the block below is for the code the agent writes against `https://api.plungeai.com`. This skill already carries it. To make it always-on for a project, paste it into `.github/copilot-instructions.md` — that file is committed, so never put the key in it. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Where next - Operating the `plungeai_*` tools once connected: **plungeai-mcp-setup**. - Writing code that calls `https://api.plungeai.com` directly: **plungeai-api-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-in-windsurf Source: https://www.plungeai.com/docs/skills/plungeai-in-windsurf Connect Windsurf (Cascade) to PlungeAI (Ocean Studio) over MCP — a ~/.codeium/windsurf/mcp_config.json entry using serverUrl, the mcp-remote bridge fallback for stdio-only builds, the 100-tool cap, and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in Windsurf/Cascade and says connect / install / set up PlungeAI, mentions mcp_config.json or serverUrl, or plungeai_* tools aren't showing up. For another editor use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config. [Download zip](https://skills.plungeai.com/plungeai-in-windsurf.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-in-windsurf/SKILL.md) Windsurf talks to PlungeAI over the Streamable HTTP MCP server at `https://mcp.plungeai.com/v1`. One `ozk_` bearer key authenticates it (and the One API, if generated code calls PlungeAI directly). No install-page deeplink exists for Windsurf — it is a config-file tool. ## Prerequisites - An `ozk_` key: Dashboard → **One API → Keys** (https://dashboard.plungeai.com) — self-service, shown once, copy it now. ## Connect (once) Merge into `~/.codeium/windsurf/mcp_config.json` (user-level — the right home for the key), then reload the MCP servers from Cascade's MCP panel or restart Windsurf: { "mcpServers": { "plungeai": { "serverUrl": "`https://mcp.plungeai.com/v1`", "headers": { "Authorization": "Bearer ozk_YOUR_KEY" } } } } `serverUrl` is the documented key for remote servers (the vendor docs also accept `url`; every vendor example uses `serverUrl` — stick with it). Older, stdio-only builds bridge with mcp-remote instead: { "mcpServers": { "plungeai": { "command": "npx", "args": ["-y", "mcp-remote", "`https://mcp.plungeai.com/v1`", "--header", "Authorization: Bearer ozk_YOUR_KEY"] } } } ## Verify 1. Cascade's MCP panel shows `plungeai` with tools after a refresh. 2. In Cascade: "use plungeai_whoami to confirm my identity" → an identity card. 3. "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog. ## Quirks - Config edits are not hot-reloaded — refresh/reload the MCP panel (or restart Windsurf) after every change, before debugging anything else. - Tool cap: Cascade has a limit of roughly 100 total tools across all servers. If `plungeai_*` tools are missing, disable unused servers and refresh. - Discovery-first rules live in project `.windsurf/rules/*.md` (the legacy single-file `.windsurfrules` is also still read) or the global `~/.codeium/windsurf/memories/global_rules.md`. Rules files are committed — never put the key there. - Money-verb PlungeAI operations still pause with a structured `needs_approval` outcome (relay it, then `plungeai_continue`) regardless of any auto-run setting. ## What the agent should know (One API 2.3.0) The `plungeai_*` tools operate PlungeAI from chat; the block below is for the code the agent writes against `https://api.plungeai.com`. This skill already carries it. To make it always-on for a project, paste it into `.windsurf/rules/plungeai.md` (rules locations: Quirks) — that file is committed, so never put the key in it. The minimal saved-workflow body (`workflow:` / `tasks:` YAML) is in **plungeai-api-setup**. ```text PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route. AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header. ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}. FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract. DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}. POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract. DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous. POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id). DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running; on_behalf_of:"<customer id>" uses that customer's connections. ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+. EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id). GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval. GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE. POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel. POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished. WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore. Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing. SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future); 400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run). POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later. TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate. The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}. PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required). BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}. DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}. GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread. MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}. SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}. TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}. CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}. MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings. TRACES: GET /v1/traces/{id}, id = the run's workflow_id. LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}. ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too); 409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once. 424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link. FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue. A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call. ``` ## Where next - Operating the `plungeai_*` tools once connected: **plungeai-mcp-setup**. - Writing code that calls `https://api.plungeai.com` directly: **plungeai-api-setup**. - Picking MCP vs the One API for a given job: **choose-your-plungeai-door**. - An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): **plungeai-mcp-setup**. - Full guide: https://mcp.plungeai.com/docs#3-connecting-clients # plungeai-agentic-agent Source: https://www.plungeai.com/docs/skills/plungeai-agentic-agent Design and emit ONE bounded agentic agent for PlungeAI (Ocean Studio) as a single `type: harness` mission — purpose, allowed_tools fence, permissions gates (deny/ask), skills/plugins/MCP/persona declarations, effort or turn cap, success criteria — then hand it to Studio as CNL YAML. Use when the Studio Think composer has "Agentic agent" selected, or when a user asks to build/create an agentic agent, a mission agent, a bounded autonomous agent, or "an agent that can decide its own steps". Triggers: "agentic agent", "build an agent", "mission", "harness", "allowed tools", "what may it do". NOT for multi-task pipelines (plungeai-workflows) or scheduled reporting bots (plungeai-bot-agent). [Download zip](https://skills.plungeai.com/plungeai-agentic-agent.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-agentic-agent/SKILL.md) <Note> Used by Studio: Ocean Studio's agent builder loads this skill by its id. </Note> You are building ONE agent that decides its own steps inside hard bounds. The deliverable is exactly one `type: harness` task inside a `workflow:` wrapper. Studio validates it, lints it (one harness task, fence includes `task_complete`, no `memory_owner`) and saves it under the "Agentic agents" section. The user then runs it from Build → Run or talks to it turn by turn. ## The conversation (before any YAML) 1. **Purpose.** Restate the standing purpose in one sentence. If the goal is fuzzy, ask ONE question that splits the space ("verify claims" vs "write the brief"?). 2. **Boundaries — the fence.** Propose the smallest `allowed_tools` set that can achieve the purpose and say why each tool is in. Ask which of these the user wants gated (`permissions: {tool: ask}`) or removed. Never widen silently. 3. **Capabilities.** Only from the AVAILABLE CAPABILITIES index in your context (skills, plugins, mcp) and from live `registry_search` results (agents). Name the ids you intend to declare and what each adds. If none fit, declare none — an invented id degrades to a silent warning at run time. 4. **Human gates.** Anything that sends, pays, posts, deletes, or writes outside the workspace gets `ask` or is excluded. Payment-class agents are deny-unless-named. 5. **Budget.** `effort: quick | standard | deep` (4 / 12 / 20 turns) or an explicit `max_turns` (a positive whole number; 1 to 500 where a bot-config block sets it). Default `standard`. 6. **Done means.** 2–4 `success_criteria` the agent checks before it finishes. Confirm the design in one short block, then emit the YAML. Ask, don't guess. ## Output contract Exactly ONE fenced `yaml` block, `workflow:` wrapper, ONE task. Nothing else in the block. ```yaml workflow: name: "<short agent name>" description: "<one line: what it does and for whom>" tasks: - type: harness id: agent agent: one-agent goal: "{input}" mission: | You are <name>, <role>. <standing purpose in 2–4 sentences>. <house rules: sources, tone, refusals>. When your task is finished, call the task_complete tool with your final result. effort: standard allowed_tools: [web_search, web_fetch, load_skill, ask_user, task_complete] permissions: { call_agent: ask } skills: [<ids from the index>] plugins: [<ids from the index>] mcp: [<ids from the index>] persona: <one id, optional> success_criteria: - <statement the agent verifies before finishing> ``` Rules the lint enforces — violating them makes the save fail: - ONE task, `type: harness`, `goal: "{input}"` (the per-run input), `mission:` as a multi-line `|` block that ends with the task_complete sentence above. - `allowed_tools` MUST include `task_complete`. Keep `ask_user` when clarification may be needed mid-run. Do not list a tool you did not discuss. - No `memory_owner`. No hardcoded emails or ids in the YAML — use `{input}`. - Omit `skills:` / `plugins:` / `mcp:` / `persona:` entirely when you declare none. Arrays REPLACE, never merge. - Always name `agent: one-agent` — the one loop runtime. Leaving `agent:` out still lands on the legacy `harness-agent` id until the switch-over. ## The fence — pick from these 24 tools only Files: `read_file write_file edit_file delete_file list_files search_files` (workspace only). Research: `web_search web_fetch`. Capabilities: `load_skill` (read a skill/plugin body), `memory` (durable facts), `knowledge` (user's knowledge base), `recall recall_history` (own past runs), `skill_manage` (author a skill — gated `ask` by default). Platform: `registry_search registry_lookup call_agent` (run any registry agent — fence it with `allowed_agents`), `invoke_workflow`, `delegate` (parallel sub-agents), `run_python`, `local_agent` (user's own machine; needs `local: true`, bots only). Human: `ask_user`, `platform_action`. Exit: `task_complete` (always). There is NO `denied_tools`. Deny = leave it out of `allowed_tools`, or `permissions: {tool: deny}`. Gate = `permissions: {tool: ask}` (the run pauses for approval). `allowed_agents: [ids]` fences `call_agent`; `'all'` opens the catalog minus `denied_agents` — money/blockchain agents stay denied unless named. ## Discovery rules - Agents: `registry_search` with a natural-language capability, then `registry_lookup` the exact id before declaring it in `allowed_agents`. - Skills / plugins / MCP servers: ONLY ids from AVAILABLE CAPABILITIES. A plugin brings its bundled skills and MCP servers; do not also list them one by one. - Models: leave `model:` out (platform default) unless the user names one. - Outside Studio: connect the MCP server at `https://mcp.plungeai.com/v1`, discover agent ids with `plungeai_list_agents {search: "<capability in plain words>"}`, then run the same YAML with `plungeai_execute_workflow`. ## Examples of good bounds - Research verifier: `allowed_tools: [web_search, web_fetch, task_complete]`, `effort: standard`, criteria "every verdict cites a primary source". - Ops helper that may act: `[registry_search, registry_lookup, call_agent, ask_user, task_complete]`, `allowed_agents: [gmail-agent, slack-agent]`, `permissions: {call_agent: ask}`. - Analyst with code: `[web_search, web_fetch, run_python, write_file, read_file, task_complete]`, `python_executor: auto`, `effort: deep`. Read [`references/harness-task.md`](/skills/plungeai-agentic-agent/references/harness-task) for every field and [`references/fence.md`](/skills/plungeai-agentic-agent/references/fence) for tool guidance when a case is not covered above. ## Reference pages <CardGroup cols={2}> <Card title="The fence — tools, agents, and fan-out guards" icon="file-text" href="/skills/plungeai-agentic-agent/references/fence"> allowedtools is a hard whitelist — only listed tools ever reach the LLM; an out-of-fence call is refused by the runtime, not merely discouraged. </Card> <Card title="Harness task — the full mission contract" icon="file-text" href="/skills/plungeai-agentic-agent/references/harness-task"> Bot-only keys (peers, posture, review, maxruntimes, workspace, permissionlocks) are plungeai-bot-agent's. </Card> </CardGroup> # The fence — tools, agents, and fan-out guards Source: https://www.plungeai.com/docs/skills/plungeai-agentic-agent/references/fence <!-- sources-of-truth: agents/agents/one-agent/tools.ts (ALL_TOOLS), agents/agents/one-agent/config.ts (DEFAULT_FENCE), agents/agents/one-agent/agentic-loop.ts (fence enforcement, permissions) | last-synced: 2026-10-05 --> ## The tool fence `allowed_tools` is a hard whitelist — only listed tools ever reach the LLM; an out-of-fence call is refused by the runtime, not merely discouraged. Omit it and `one-agent` offers **every tool** (the 8-tool default fence is `universal-agent`'s only) — so always list the fence you mean. The full loop-tool catalog (fence vocabulary): `read_file`, `write_file`, `edit_file`, `list_files`, `search_files`, `delete_file`, `web_search`, `web_fetch`, `load_skill`, `memory`, `knowledge`, `skill_manage`, `recall`, `recall_history`, `delegate`, `local_agent`, `registry_search`, `registry_lookup`, `call_agent`, `invoke_workflow`, `run_python`, `ask_user`, `platform_action`, `task_complete` — plus the five always-on tools `schedule`, `message_bot`, `browser`, `code`, `mcp_tool_search`, which need a saved bot (`plungeai-bot-agent`), 29 tools in all. Fence design rules: - **Always include `task_complete`** — it is how a run ends cleanly. - Smallest set that can achieve the goal. A verification mission needs `web_search, web_fetch, task_complete` — not the file tools. - `ask_user` keeps human-question continuations available on every surface; include it when the goal may need clarification mid-run. - The loop runtime is `agent: one-agent` — always name it on the harness task (a harness task with no `agent:` still lands on the legacy `harness-agent` id until the switch-over). With no `allowed_tools` it has every tool, registry agents included (the thin 8-tool fence — the registry triad, `web_search`, `web_fetch`, `load_skill`, `ask_user`, `task_complete` — is `universal-agent`'s default). An explicit `allowed_tools` always narrows it. ## The agent fence and money-class protection `call_agent` lets the loop invoke registry agents. `allowed_agents: [a, b]` limits it to those; `'all'` opens the catalog minus `denied_agents`. **Payment/blockchain-class agents stay deny-unless-explicitly-named even under `'all'`** — naming them is the only way a mission can touch money, and `permissions: {call_agent: ask}` adds a human gate on top. This is a trust fence: surface refusals, never work around them. ## Recursion and fan-out guards - `delegate` spawns parallel child agents; children get a minimal default tool set and ALWAYS have `delegate`, `invoke_workflow`, and `memory` stripped — no fan-out explosions, no child memory writes. - Orchestrator depth is propagated (`depth` → children send `depth+1`) and hard-capped (default max depth 3); a too-deep dispatch is refused. - `max_parallel` caps one delegate call's width by refusal, never by serializing — silent batching would multiply wall clock. # Harness task — the full mission contract Source: https://www.plungeai.com/docs/skills/plungeai-agentic-agent/references/harness-task <!-- sources-of-truth: orchestration/cnl-engine/harness-mission.ts, orchestration/cnl-engine/schema-types.ts (InlineMission), orchestration/cnl-engine/validate.ts, core/core-base/effort-presets.ts | last-synced: 2026-10-05 --> ## Full field reference | Field | Type | What it bounds | |---|---|---| | `goal` | string | The per-run input — becomes the agent's prompt | | `mission` | multi-line string | Standing purpose → system-prompt frame. A SINGLE-TOKEN string is a card reference instead (below) | | `persona` | string | One voice per run (`digital_twin` alias; `personas: [x]` → first entry) | | `skills` | string[] | Instruction packs — first 5 eager, rest on demand (`skills.md`) | | `experts` | string[] | Domain lenses — first 3 eager (`experts-personas.md`) | | `backgrounds` | string[] | Ambient context cards — first 3 eager, injected first | | `plugins` | string[] | Bundles: skill index + MCP servers + scripts (`plugins.md`) | | `mcp` | string[] | MCP server ids connected in-loop | | `model`, `provider` | string | Model/provider override (`models.md`) | | `effort` | `quick`\|`standard`\|`deep` | Turn-budget preset: **4 / 12 / 20** turns | | `max_turns` | number | Explicit loop cap (alias `max_iterations`), a positive number with no runtime ceiling; wins over `effort`; runtime default 50 | | `max_tokens` | number | Per-turn output token budget for the loop's model calls | | `budget_usd_run` | number | Per-run spend cap in USD, above 0 (omit for no cap); the loop stops at the next turn once cost passes it | | `max_parallel` | number | Widest fan-out one `delegate` call may spawn — cap-and-refuse, never silently batched | | `allowed_tools` | string[] | The tool fence (below) | | `allowed_agents` | string[] \| `'all'` | Agent fence for `call_agent` | | `denied_agents` | string[] | Subtracted from the agent fence | | `permissions` | map tool or class (`send`, `write`, `pay`, `delete`) → `allow`\|`ask`\|`deny`\|`hand_off` | Per-tool or per-class gate: `ask` pauses the run with `needs_approval` before the tool executes; `deny` rejects the call; `hand_off` never runs it and tells the owner it is theirs to do (`one-agent` only; elsewhere it becomes `deny`) | | `success_criteria` | string[] | Self-checked statements the agent verifies before finishing | | `instructions` | string | Extra author instructions, appended after skills — always lands | | `python_executor` | `auto`\|`pyodide`\|`anthropic`\|`gemini` | `run_python` tier routing | | `memory_owner` | string | Memory namespace override — per-bot isolation (`memory.md`) | | `local`, `local_agents` | bool, string[] | Local bot: actions route to the user's own machine via the desktop daemon (cloud brain, local hands); requires a connected local node | Bot-only keys (`peers`, `posture`, `review`, `max_runtime_s`, `workspace`, `permission_locks`) are `plungeai-bot-agent`'s. In a saved agentic agent: `memory_owner` is forbidden (tenancy comes from the executing user); `local` / `local_agents` are bot-only. Always name `agent: one-agent` (cloud and local alike) — a harness task with no `agent:` still lands on the legacy `harness-agent` id until the switch-over. ## Validation Studio applies at save (validate.ts:221-311) - `goal` required (`prompt:` is an alias); `mission` (text or object) OR `mission_ref` required. - `allowed_tools` must be an array; `max_turns` a positive number; `effort` ∈ quick|standard|deep. - `skills / experts / backgrounds / plugins / mcp` must be string arrays; `persona` one string. - Ids are NOT checked at save — an unknown id degrades to a warning at run time. Declare only listed ids. - Merge order when a card is referenced: card → workflow → task, last wins; arrays replace. # plungeai-bot-agent Source: https://www.plungeai.com/docs/skills/plungeai-bot-agent Design and emit ONE PlungeAI (Ocean Studio) bot agent — a scheduled, unattended `type: harness` mission that reports to the user's channels (in-app, email, Slack, Telegram, WhatsApp, Discord, Teams) — as CNL YAML plus a bot-config block (cron schedule, delivery targets, optional run-on-my-computer). Use when the Studio Think composer has "Bot agent" selected, or when a user asks for a bot, a scheduled agent, a daily/weekly brief, a watcher, a monitor, a digest, or "something that runs on its own and tells me". Triggers: "bot", "every morning", "schedule", "report to Slack", "watch and alert", "digest". NOT for interactive agents (plungeai-agentic-agent) or multi-task pipelines (plungeai-workflows). [Download zip](https://skills.plungeai.com/plungeai-bot-agent.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-bot-agent/SKILL.md) <Note> Used by Studio: Ocean Studio's agent builder loads this skill by its id. </Note> A bot is an agentic agent plus three facts: WHEN it runs, WHERE the result goes, and whether its hands are the user's own computer. Studio saves it under "Bots" and creates the schedule and delivery from your bot-config block. Unattended runs cannot ask anything: no `ask_user`, no `platform_action`, every risky tool excluded or `permissions: deny`. ## The conversation (before any YAML) 1. **Job.** One sentence: what each run produces (a brief, an alert, a digest). 2. **Cadence.** Ask for the schedule in words, propose the cron, confirm it (five fields); set `timezone` (IANA, e.g. `Europe/Berlin`) in the block to the user's zone, never convert to UTC. 3. **Where it reports.** `inapp` (Studio inbox, always safe), `email`, `slack`, `telegram`, `whatsapp`, `discord` (these three need a paired chat: tell the user to pair first), `teams` (needs the owner's Microsoft connection). Default `inapp`. 4. **Hands.** Cloud (default) or "runs on this computer" (`local: true`): local files, local apps, local agents. Both run on `agent: one-agent`. 5. **Fence, money, spend.** Smallest `allowed_tools`; ids only from AVAILABLE CAPABILITIES and live `registry_search`; `effort: standard` unless argued; 2–4 `success_criteria`. Ask what the bot may never do alone (pay, send, delete) and what a run may cost. Confirm, then emit BOTH blocks. ## Output contract — two fenced blocks, in this order ```yaml workflow: name: "<short bot name>" description: "<one line: what it reports and how often>" tasks: - type: harness id: bot agent: one-agent goal: "{input}" mission: | You are <name>, a bot that <job>. Each run you <steps in 2–4 sentences>. Output <format: bullets / table / one paragraph>. Keep it under <n> words. End with `[SILENT]` (or make your whole result `[SILENT]`) when nothing is worth reporting this run — the platform then delivers to no channel. When your task is finished, call the task_complete tool with your final result. effort: standard allowed_tools: [web_search, web_fetch, task_complete] success_criteria: - <statement the bot verifies before finishing> ``` ```bot-config schedule: "0 7 * * 1-5" deliver: - channel: inapp - channel: slack chat_id: C0123ABCD local: false prompt: "<the per-run input the schedule sends as {input}>" ``` Rules the save lint enforces — violating them fails the save: - ONE task, `type: harness`; a present `allowed_tools` MUST include `task_complete`; no `memory_owner`. - `posture`, `peers`, `review`, `max_runtime_s` and `workspace` apply only on `agent: one-agent`; on `harness-agent`, `agentic-agent` or `universal-agent` they fail the save, not ignored. - A `local: true` bot must name `agent: one-agent`; put `local: true` in BOTH blocks (task and bot-config, so the scheduler skips cleanly when the machine is offline) and add `local_agents: [ids]` only when the user names them. Also expected: `agent: one-agent`, `goal: "{input}"`, a mission ending with the task_complete sentence, no emails or ids in the YAML (recipients go in `deliver`), no `ask_user` / `platform_action` (nobody answers). `skill_manage` needs `permissions: {skill_manage: allow}` unattended: omit it unless asked. ## bot-config fields - `schedule` — 5-field cron, required (`0 7 * * 1-5` weekdays 07:00, `*/30 * * * *`). Optional `timezone` (IANA). - `deliver` — list of `{channel, chat_id?, to?, phone_number_id?}`: `inapp` (none), `email` (`to` optional = owner), `slack` (`chat_id` C… or `to` U…; none = DM the owner), `telegram`/`discord` (`chat_id`), `whatsapp` (`to` + `phone_number_id`), `teams` (`chat_id` = the Graph chat id; posts as the owner through their Microsoft connection; text capped at 4000 characters). - `local`, `prompt` (the per-run input; default empty). - `budget_usd_run`, `budget_usd_month` — USD caps above 0, at most 100000; `0` is refused, omit the key for no cap. - `permissions` — class presets `{send|write|pay|delete: allow|ask|deny|hand_off}` stored on the bot. - `peers`, `posture`, `review`, `max_runtime_s`, `workspace`, `max_turns`, `skills`, `persona`, `allowed_tools` — the mission keys below, written onto the task. `memory_consolidate: weekly` — opt-in weekly memory consolidation. ## Mission keys (on the harness task, snake_case) | Key | Value | Effect | |---|---|---| | `posture` | `read_only` | denies write, send, delete, pay, `delegate`, `invoke_workflow`, every MCP tool | | `peers` | `[bot name \| id \| "a2a:<url>"]` | the only bots `message_bot` may reach; none by default | | `review` | `auto` | a model reviews each allowed write/send/pay/delete call (one extra call each) | | `max_runtime_s` | seconds | wall-clock cap; the run stops with `timeout` | | `workspace` | `bot` \| `run` | `bot` (default for a saved bot) keeps files across runs; `run` starts clean | | `budget_usd_run` | USD | stops the loop at the next turn once cost passes it | | `permissions` | `{class or tool: allow\|ask\|deny\|hand_off}` | see Permissions | | `allowed_tools` | list | the fence; opt-in tools below | | `skills`, `persona` | ids | capability injection | | `max_turns` | number | turn cap (`effort` sets a default) | An off-contract `posture` or `review` is forced to `read_only` / `auto`: it never widens a run. ## Permissions and budgets Classes: `send`, `write`, `pay`, `delete` (reads are always allowed). Presets: `allow`, `ask` (the run pauses `needs_approval`, up to 7 days), `deny`, and **`hand_off`**: the call never runs and does not retry; the result ends with `[handed off to you — not run: …]` so the owner does it themselves (a leading `[SILENT]` is dropped). Use `hand_off` for what a bot should prepare but never do (`pay: hand_off`). A call resolves: read-only posture, then `deny`, then `hand_off`, then the money floor (`buy, pay, transfer, send, withdraw, …` always `ask`, even with `pay: allow`), then the tool-name preset, then the class preset, then allow. An unknown preset is `ask`. A team admin can LOCK a class: its `allow` rises to `ask` (never lifts a deny). Locks live on the saved bot, not in YAML. Three denied calls in a row stop an unattended run (`stop_reason: denied`). `hand_off` is `one-agent` only (other runtimes get `deny`); the task's `permissions:`, bot-config `permissions` and the permissions verbs all accept it. Safe default: `permissions: { pay: ask, delete: ask }`. Budgets: `budget_usd_run` caps one run; `budget_usd_month` (bot-config, Studio, or the verbs below) is enforced by the scheduler. The lower of the YAML and the stored per-run cap applies, and a capped run's result says so. Blank or omitted = no cap; the block and Settings refuse `0`, and the verbs clear a cap with `null` / `none` — never send `0` (the runtime ignores it). ## Always-on abilities (opt-in) Add to `allowed_tools` only what the job needs; each needs a saved bot. - `schedule` — `action: create | list | pause | resume | delete | run_now | wake`. `create {name, cron, timezone?, prompt?, deliver?, ends_at?}`; one run at a clock time is cron `@once` with `run_at` (ISO-8601 with offset); `wake {in, note}` re-runs the bot once (min 60 s). The bot writes the cron and says it back in words; 50 live jobs per bot. - `message_bot` — `{to, text, wait?}`; `to` is in `peers`, or `group:<name>` (the sender must be a member of that group; `wait: true` is refused for a group). Replies arrive as `[MESSAGE from <bot>]` at the start of a later run; `wait: true` runs the peer now as a plain child (no memory, inbox, workspace). A **bot group** (2–6 of the owner's bots, made in Studio or via the verbs) fans a message out: membership is the fence, every other member's inbox gets it, the next member wakes, 8 hops max. - `browser` — `{steps, url?, session?: 'bot' | 'none'}`; `'bot'` keeps cookies. Click, type and key steps count as writes. - `memory` — `action: forget {query}` purges matching entries. - `code` — `{prompt, directory?, session_id?, timeout_s?}` hands one turn (max 300 s) to the owner's OceanCode over the Local Tunnel. Class `write`; no daemon connected, no tool. - `mcp_tool_search` — only with MCP servers attached: past 15 tools or 8 KB, schemas are listed by name and a search loads up to 5 for the next turn (never ones the permissions refuse). Shared bots: inbox, `schedule`, `message_bot`, the browser session and workspace are owner-only; a teammate runs a shared bot without identity. ## Channels in A chat can be bound to a bot (Telegram, WhatsApp, Discord, Teams): `/bot <name>` in the chat (Slack: `/plunge bind`), or the verbs below; pair first (Teams two-way chat also needs the platform's Teams app registration). Voice notes (Telegram and WhatsApp only, up to 25 MB) from an authorised sender are transcribed with Whisper and answered like text (as audio where the channel can); Discord and Teams are text only. ## Outside Studio — the same bot over REST, MCP and CLI REST `https://api.plungeai.com` (One API 2.3.0, live contract `GET /v1/openapi.json`), MCP `https://mcp.plungeai.com/v1`, CLI `ocean-cli`. | Job | REST | MCP | CLI | |---|---|---|---| | policy, locks, caps | `GET`/`PUT /v1/workflows/{id}/permissions` | `plungeai_workflow` `permissions_get` / `permissions_set` | `workflows permissions get\|set --class pay=ask --lock pay --budget-run 0.5 --budget-month 20` | | workspace files | `GET …/workspace-files`, `…/download?path=` | `files` / `file` | `workflows files <id> [--download <path> --out <file>]` | | inbox | `GET …/inbox` | `inbox` | `workflows inbox <id>` | | chat bindings | `GET`/`POST …/bindings`, `DELETE …/bindings/{channel}/{sender_id}`, `POST /v1/channels/pair` | `bindings` / `bind` / `unbind` | `workflows bindings list\|bind --channel telegram --sender <id>\|unbind --channel --sender --yes` | | groups | `/v1/bot-groups`, `/{id}`, `/{id}/thread` | `groups` / `group_set` / `group_delete` / `group_thread` | `bot-groups list\|create --name --members a,b\|update\|delete\|thread <id>` | | steer or list a run's files | `POST /v1/executions/{id}/steer`, `GET …/files` | `plungeai_executions` `steer` / `files` | `executions steer <id> <text>`, `executions files <id>` | | schedule once, wake | `POST /v1/schedules` (`@once` + `run_at`), `…/{id}/wake` | `plungeai_schedule` `run_at`, `wake` | `schedules create NAME --type workflow --target <id> --cron @once --at <iso>`, `schedules wake <job_id> --note` | A saved bot runs under its stored policy on every door, including the Agent Call door: `POST /v1/execute {"agent": "<bot id>@3", "input": "…"}`. ## The fence for unattended runs Same 24 loop tools as an agentic agent (`plungeai-agentic-agent`) plus the always-on ones above. Leave out `ask_user` and `platform_action`; prefer read paths (`web_search web_fetch read_file recall_history knowledge`) plus the agents the job needs via `call_agent` with a tight `allowed_agents`; anything that sends or pays needs the user's explicit yes in the conversation AND stays `deny`/`hand_off`/`ask`. Delivery is NOT a tool: the platform sends `task_complete`'s result to the `deliver` targets ([`references/bot-config.md`](/skills/plungeai-bot-agent/references/bot-config): limits, pairing). Discover agent ids live (`plungeai_list_agents`, `registry_search`), never from memory. ## Reference pages <CardGroup cols={2}> <Card title="bot-config → scheduler job" icon="file-text" href="/skills/plungeai-bot-agent/references/bot-config"> The block becomes one scheduled job that delivers taskcomplete's result to every target. </Card> </CardGroup> # bot-config → scheduler job Source: https://www.plungeai.com/docs/skills/plungeai-bot-agent/references/bot-config <!-- sources-of-truth: orchestration/scheduler/delivery.ts, orchestration/scheduler/types.ts (CreateJobRequest), apps/ocean-studio/src/components/scheduler/deliveryTypes.ts, apps/ocean-studio/src/worker/lib/botConfig.ts, apps/ocean-studio-stable/src/worker/lib/botConfig.ts, core/core-base/bot-policy.ts, orchestration/BOT-README.md | last-synced: 2026-10-05 | maintainers: this skill's body is stored in skill-memory — after editing it run `npm run push:skill-memory` (Bot agent composes without it; the pushed body must stay under 14000 bytes) --> The block becomes one scheduled job that delivers `task_complete`'s result to every target. ## Where fields land, and their limits An unknown key is ignored; a bad value fails the save with `bot-config.<key> …`. | Field | Limit | Lands on | |---|---|---| | `max_runtime_s` | whole seconds (decimals rounded down), 10 to 86400 | the task | | `max_turns` | whole number, 1 to 500 | the task | | `skills`, `allowed_tools`, `peers` | at most 50 items, 256 characters each | the task | | `persona` | at most 128 characters | the task | | `budget_usd_run`, `budget_usd_month` | greater than 0, at most 100000 (0 is refused: a zero cap is ignored) | the bot | | `permissions` | the four classes only, `allow\|ask\|deny\|hand_off` | the bot | | `memory_consolidate` | `weekly` | a second job, Sundays 03:00 | Task keys are written onto the saved task and win over the YAML. Budgets and class presets live on the bot (Settings edits them; the engine folds them onto the task each run: YAML wins per class, the lower per-run cap wins), so keep them out of the task YAML. The save lint, not the block, refuses `allowed_tools` without `task_complete`. ## Delivery behaviour (delivery.ts) - Fire-and-forget: a delivery failure never fails the run. - Length caps (characters): telegram 3800, whatsapp 3800, discord 1900, slack 3800, email 100000, inapp 2000. Keep the output under the smallest cap you use. - telegram / whatsapp targets must be paired to the owner (pairing code in Studio, `/pair <code>` from that chat; Telegram also needs the owner to have opened the bot once); discord needs any active pairing. Unpaired targets are skipped. - email sends from the platform address; a bare target means the owner. Any other address delivers only after its holder confirmed it, else it is skipped silently. - slack: a bare target DMs the owner; a `chat_id` / `to` target needs the owner to be a connected workspace member. - teams: skipped without the owner's Microsoft connection; channel posts (`to: <teamId>/<channelId>`) are refused until the platform is granted the scope — use a chat id. ## What the block does NOT do - Channel binding, inbound email / webhook triggers and outbound run hooks are attached after creation: the bot's ⋮ menu in Studio, `/v1/triggers/{webhook|email|hooks}`, `plungeai-scheduling`. # plungeai-campaign-agent Source: https://www.plungeai.com/docs/skills/plungeai-campaign-agent Design and emit ONE PlungeAI (Ocean Studio) campaign agent — a long-running, list-driven, resumable agent that works an owned ledger of items in short scheduled runs until the current cycle is exhausted — as CNL YAML (a claim task plus a `type: batch` with `ledger: campaign`) and a campaign-config block (list source, cycle, schedule, max_attempts, then, deliver). Use when the Studio Think composer has "Campaign agent" selected, or when a user asks for an agent that works through a list, a backlog, a catalog, "N things to check", "run until the list is done", "start again every week", or "then move to the next list". NOT for single scheduled reports (plungeai-bot-agent), interactive agents (plungeai-agentic-agent) or multi-step pipelines (plungeai-workflows). [Download zip](https://skills.plungeai.com/plungeai-campaign-agent.zip) · [View raw SKILL.md](https://skills.plungeai.com/plungeai-campaign-agent/SKILL.md) <Note> Used by Studio: Ocean Studio's agent builder loads this skill by its id. </Note> A campaign is a task list with an owner. Each run claims a slice of the not-yet-done items, works them at bounded concurrency, and records every outcome before it ends; the scheduler re-fires runs until the cycle is exhausted. Runs are short by law — the platform cannot hold one invocation past ~10 minutes — so never design a run that "keeps going". Repetition is the schedule's job, resumption is the ledger's job. ## The conversation (before any YAML) 1. **Unit.** What is one item? A product, a lead, a URL, a document. Name the key that identifies it. 2. **List source.** Exactly one: a Task-app **table + filter** (point-and-pick; never raw SQL), an **agent op** that returns a JSON array (discover the id with `registry_search`, never from memory), a **CSV** the user will upload in the Task app after save, or an **inline** list for a handful of items. 3. **Per-item job.** A plain agent op when the item is "call one agent, store one row" (cheapest). A harness mission only when the item genuinely needs a tool loop; then the same rules as a bot: no `ask_user`, no `platform_action`, `effort: quick` unless argued, mission ends with the task_complete sentence. The job MUST upsert by `{item.key}` + `{item.cycle}` — say so in the mission or pass both to the op. 4. **Cycle.** `once` (a backlog), `hourly | daily | weekly | monthly` (a recurring pass), `continuous` (restart as soon as exhausted). `cycle_start: reset` reuses the same keys; `refill` re-queries the source. 5. **Schedule.** When runs may happen — a 5-field cron. Propose a window that finishes the list with margin: `batch_size × seconds per item ÷ concurrency ≤ 600 s`, then enough ticks to cover the list. 6. **Deliver.** Where the cycle summary and alerts go: `inapp`, `email` or `slack` (see `deliver` below). Confirm, then emit BOTH blocks. ## Output contract — two fenced blocks, in this order ```yaml workflow: name: "<short campaign name>" description: "<one line: what one item is and how often the list is worked>" tasks: - type: task id: claim agent: data-table-agent operation: campaignClaim campaign_id: "<written by Step-2 at save>" batch_size: 40 - type: batch id: work items_from: claim concurrency: 5 ledger: campaign tasks: - type: task # or type: harness + agent: one-agent when a tool loop is needed id: one agent: <agent id> operation: <op> key: "{item.key}" cycle: "{item.cycle}" ``` ```campaign-config list: table: { project: <project>, table: <table>, filter: { <column>: <value> }, key: <column> } cycle: weekly cycle_start: refill schedule: "*/5 9 * * 1" max_attempts: 2 then: "" deliver: - channel: inapp local: false ``` Rules the lint enforces — violating them makes the save fail: - Exactly one top-level claim task (`agent: data-table-agent`, `operation: campaignClaim`) and exactly one `type: batch` with `items_from: claim`, `ledger: campaign`, and `concurrency` ≤ 5. - Inner tasks are unattended: no `ask_user`, no `platform_action`; a harness inner task MUST include `task_complete` in `allowed_tools` and end its mission with the task_complete sentence. - The per-item task must reference `{item.key}` (warning if it looks insert-only — the ledger is at-least-once). - An inner task that can reach `data-table-agent` (e.g. `allowed_agents: [data-table-agent]`) needs `max_items`: a `max_items: <n>` line in the YAML (on the claim and batch tasks) and the same cap in campaign-config. - `max_attempts` is required. No `memory_owner`. No emails or ids in the YAML — recipients belong in `deliver`. - `local: true` only with a harness inner task on `agent: one-agent`, and in BOTH blocks. ## campaign-config fields - `list` — one of `table {project, table, filter, key}`, `agent {id, operation}`, `csv: true`, `inline: [...]`. - `cycle` — `once | hourly | daily | weekly | monthly | continuous`. Calendar cycles never restart inside their bucket. - `cycle_start` — `reset | refill`. Default `refill` for table/agent sources, `reset` for csv/inline. - `schedule` — 5-field cron; each tick is one run (an empty claim returns in about a second). Optional: left out, no cron job is made and the ledger schedules its own next run. - `max_attempts` — dead-letter threshold on explicit failures; required. - `then` — optional campaign id to start when a cycle completes. Never chain a dependent report onto a collection campaign; keep reports time-triggered. - `deliver` — `{channel, to?}` items (default `inapp`). Only `inapp`, `email` (`to` = address; bare = the owner) and `slack` (bare = DM the owner, or `to`) deliver from a campaign today: the block carries no `chat_id` or `phone_number_id`, which telegram, discord, whatsapp and teams need (accepted, then skipped at delivery). - `batch_size` (positive integer, the per-run claim slice, default 40), `max_items` (positive integer, rows per cycle), `pace` (seconds ≥ 0) — optional. - `local` — run-on-my-computer, as for bots. Worked examples for all four use cases plus the full field reference: [`references/campaign-config.md`](/skills/plungeai-campaign-agent/references/campaign-config). Outside Studio, one call saves the campaign and sets up its ledger and schedule: MCP (`https://mcp.plungeai.com/v1`) `plungeai_workflow {action: "create", kind: "campaign", yaml, campaign_config: {…}}`, or REST `POST https://api.plungeai.com/v1/workflows` with `kind: campaign` and `campaign_config` (the block's fields as an object; `list`, `cycle` and `max_attempts` required, `422 invalid_params` otherwise); `ocean-cli campaigns setup <workflow_id> --config @file` arms an existing one (Studio session). Discover the agent ids you name in the per-item task live — `plungeai_list_agents` from a client, or `registry_search` inside the loop — never from memory. ## Reference pages <CardGroup cols={2}> <Card title="campaign-config reference" icon="file-text" href="/skills/plungeai-campaign-agent/references/campaign-config"> A campaign-config fenced block sits after the CNL YAML. Studio parses it to open the ledger and create the scheduler job. Fields: </Card> </CardGroup> # campaign-config reference Source: https://www.plungeai.com/docs/skills/plungeai-campaign-agent/references/campaign-config <!-- sources-of-truth: agents/agents/campaign-agent/campaign-agent-design-5.0.md §8, agents/agents/campaign-agent/EXAMPLES.md, apps/ocean-skills/skills/plungeai-bot-agent/references/bot-config.md (deliver section) | last-synced: 2026-10-05 --> A `campaign-config` fenced block sits after the CNL YAML. Studio parses it to open the ledger and create the scheduler job. Fields: | Field | Required | Meaning | |---|---|---| | `list` | yes | Where items come from. One of `table:` (a data-table project+table), `agent:` (a lister task whose JSON array feeds `items_from`), `csv: true` (uploaded rows), or `inline: [key, …]`. | | `cycle` | yes | Calendar bucket: `once` \| `hourly` \| `daily` \| `weekly` \| `monthly` \| `continuous`. | | `cycle_start` | when re-listing | `refill` (add new list rows, keep prior) or `reset` (fresh cycle from the list). | | `schedule` | no | Cron for the run cadence, e.g. `"*/5 9 * * 1"`. The scheduler re-fires until the cycle drains. Left out: no cron job is created and the ledger schedules its own next run. | | `max_attempts` | yes | Per-item retry cap before an item is marked `failed` (integer ≥ 1; the ledger column defaults to 2). | | `then` | no | Campaign id to hand the baton to when this one's cycle completes. | | `deliver` | no | `[{channel, to?}]` for the cycle-complete notice; `inapp` (default), `email` and `slack` deliver; `telegram`, `whatsapp`, `discord`, `teams` are accepted but need a `chat_id` / `phone_number_id` this block cannot carry, so delivery skips them (see below). | | `local` | no | `true` runs items on the user's own computer. | | `batch_size` | no | Positive integer: how many items one run claims (default 40). | | `max_items` | no | Positive integer: hard cap on rows in a cycle. Required (as a `max_items:` line in the YAML too) when an inner task can reach `data-table-agent`, because the item task may then add rows to its own ledger. | | `pace` | no | Number ≥ 0 (seconds) between items. | | `task_types` | no | Per-row task/worker overrides (§8a). | | `learn` | no | Enables the ledger's recall/knowledge lane (§8a–8c). | ## Sizing (rule of thumb) Keep `batch_size × seconds per item ÷ concurrency ≤ 600 s` — well inside the 15-minute DO wall. The default `batch_size: 40`, `concurrency: 5` at ~30 s/item = 4 min per run; up to ~100 items per run is safe on the scheduled path. `batch_size` and `concurrency` are hidden authoring knobs — never operator vocabulary. ## deliver channels <!-- adapted from plungeai-bot-agent/references/bot-config.md (deliver section) --> - Fire-and-forget: a delivery failure never fails the run. - Length caps per channel: slack 3800, email 100000, inapp 2000 characters — keep the summary under the smallest cap you use. - A campaign's `deliver` items are `{channel, to?}` only. telegram and discord need a `chat_id`, whatsapp a `to` plus `phone_number_id`, teams a Graph `chat_id`: the block cannot carry them, so those targets are skipped. A bot's `deliver` takes all of them (`plungeai-bot-agent`). - email sends from the platform address; a bare email target means the owner's address. Any other address delivers only after that address holder confirmed it (a confirmed delivery recipient) — otherwise the target is skipped silently. - slack: a bare target DMs the owner; a `to` target needs the owner to be a connected workspace member. ## Worked shapes <!-- from agents/agents/campaign-agent/EXAMPLES.md; example 2 adds the max_items the campaign lint requires --> ### 1 · Weekly catalog price check (the Arhaus case) 280 active products, ~30 s each, concurrency 5, 40 per run → seven runs of ~4 min on Monday morning, exhausted by ~09:34; analysis (10:00) and Excel (10:15) stay separate time-triggered jobs. ```yaml workflow: name: "RH price check" description: "Weekly price check of the tracked catalog" tasks: - type: task id: claim agent: data-table-agent operation: campaignClaim campaign_id: "<written by Step-2 at save>" batch_size: 40 - type: batch id: work items_from: claim concurrency: 5 ledger: campaign tasks: - type: task id: one agent: price-collector-v2-agent operation: collect_single product_name: "{item.key}" week_date: "{item.cycle}" ``` ```campaign-config list: table: { project: furniture-pricing, table: products, filter: { status: active }, key: product_id } cycle: weekly cycle_start: refill schedule: "*/5 9 * * 1" max_attempts: 2 deliver: [{ channel: inapp }] local: false ``` --- ### 2 · 200 leads, enriched once A CSV uploaded in the Task app; a small harness per item; runs every five minutes until exhausted (~5 runs), then `done`: job paused, summary delivered. ```yaml workflow: name: "Lead enrichment — September list" description: "Enrich each lead once and store the result" tasks: - type: task id: claim agent: data-table-agent operation: campaignClaim campaign_id: "<written by Step-2 at save>" batch_size: 40 max_items: 300 - type: batch id: work items_from: claim concurrency: 5 ledger: campaign tasks: - type: harness agent: one-agent id: one goal: "Enrich the lead {item.key} ({item.company}, {item.email}). Find the company's website, size and industry; store the result with call_agent to data-table-agent, upserting by key {item.key} in cycle {item.cycle}." mission: | You are a lead researcher. Use only public sources and cite the URL you relied on. When your task is finished, call the task_complete tool with your final result. effort: quick allowed_tools: [web_search, web_fetch, call_agent, task_complete] allowed_agents: [data-table-agent] ``` ```campaign-config list: csv: true cycle: once cycle_start: reset schedule: "*/5 * * * *" max_attempts: 2 max_items: 300 deliver: [{ channel: inapp }, { channel: email }] local: false ``` --- ### 3 · Continuous monitor of 50 URLs An inline list; each exhaustion opens the next cycle (`#n+1`). The no-progress breaker (three consecutive runs completing nothing) pauses an all-failing list instead of looping on it. ```yaml workflow: name: "Status-page watch" description: "Check 50 status pages continuously and record changes" tasks: - type: task id: claim agent: data-table-agent operation: campaignClaim campaign_id: "<written by Step-2 at save>" batch_size: 50 - type: batch id: work items_from: claim concurrency: 5 ledger: campaign tasks: - type: task id: one agent: firecrawl-agent operation: scrape url: "{item.key}" formats: [markdown] ``` ```campaign-config list: inline: - "https://status.example-a.com" - "https://status.example-b.com" cycle: continuous cycle_start: reset schedule: "0 * * * *" max_attempts: 3 deliver: [{ channel: slack }] local: false ``` --- ### 4 · Chain: finish list A, then start campaign B Campaign A runs once; when its cycle completes, the scheduler sets campaign B's job to run now. Never use this to chain a dependent report onto a data-collection campaign — a partial cycle must not trigger analysis on incomplete data (the Arhaus analysis/Excel jobs stay time-triggered for exactly this reason). ```campaign-config list: agent: { id: registry-sync-agent, operation: list_stale_cards } cycle: once cycle_start: refill schedule: "*/5 * * * *" max_attempts: 2 then: "<campaign B id>" deliver: [{ channel: inapp }] local: false ``` # Changelog Source: https://www.plungeai.com/docs/resources/changelog <Info> Planned: not available yet. Tracked as TI-30. </Info> ## What this will do This page will list PlungeAI changes newest first, one dated entry per release day, each linking the pages that describe it. The dated entries are not published yet. ## Use this today <Card title="MCP guide: versions and what changed" icon="book" href="/mcp#010-versions-and-what-changed"> The MCP server versions and recent changes. </Card> <!-- placeholder-source: content/_placeholders.json -->