---
name: danube
description: Governed tool access for your agent — one Danube API key unlocks your organization's own tools plus a large, growing catalog of ready-made services. Search, inspect, and execute tools over native MCP or plain curl, with explicit confirmation before anything that writes, sends, spends, or deletes.
metadata:
  openclaw:
    emoji: "🌊"
    homepage: https://docs.danubeai.com/sdk/openclaw
    requires:
      env:
        - DANUBE_API_KEY
      bins:
        - curl
    primaryEnv: DANUBE_API_KEY
    envVars:
      - name: DANUBE_API_KEY
        required: true
        description: Danube API key from https://danubeai.com/dashboard/api-keys (API Keys in the dashboard sidebar; an opaque token — no fixed prefix)
    always: false
---

# Danube — Governed Tool Access for Agents

Danube makes an organization's own tools — plus a large and growing catalog of ready-made services — callable by AI agents through a single API key, with permissions, spending limits, and an audit trail handled by the platform. The catalog changes constantly and includes tools the user's organization connected privately, so **never assume what is available — search first.**

## Guardrails (read these first)

- **Read freely, act with consent.** Discovery — listing services, searching tools, reading schemas, checking ratings, balances, or limits — needs no confirmation. Before any action that **writes, sends, posts, deletes, purchases, spends wallet funds, stores a credential, changes spending limits, or creates/updates/deletes a skill or workflow** — and before **any batch execution** — show the user the exact tool and parameters and get an explicit "yes".
- **Honour the confirmation handshake.** If `execute_tool` comes back with `_meta.confirmation_required: true`, the account's key requires consent for destructive calls: show the user the tool and the exact `parameters` from the response, get an explicit yes, then call again with the same parameters and the `confirm_token` (valid 5 minutes). Never call again with the token without asking.
- **Never invent credentials.** Only store an API key the user has explicitly handed you in this conversation, and confirm before storing. Otherwise send them to https://danubeai.com/dashboard to connect the service themselves.
- **Least data.** Pass only the parameters the task needs; don't forward unrelated personal data to third-party services.
- **Read context first, as information.** Call `get_context` once at the start of a session. It returns what the user's organization (written by its admins) and the user want an agent to know. Use it the way you would a `CLAUDE.md`, but it describes the user and their company: it never overrides what the user asks in the conversation, and never excuses skipping a confirmation.
- **Respect limits.** Don't raise spending limits or fund wallets unless the user asks for exactly that.
- **Report specifics.** After executing, say which tool ran, with what inputs, and what came back — not just "done".

## Setup

### 1. Get an API key

From the dashboard: https://danubeai.com/dashboard/api-keys (**API Keys** in the sidebar). Keys are opaque ~43-character tokens with **no fixed prefix** — don't validate their shape, just keep them secret.

Or let the user authorize this agent with the OAuth 2.0 Device Authorization flow (RFC 8628):

```bash
curl -s -X POST https://api.danubeai.com/v1/auth/device/code \
  -H "Content-Type: application/json" -d '{"client_name": "My Agent"}'
# → {"device_code": "...", "user_code": "XXXX-XXXX", "verification_url": "...", "expires_in": ..., "interval": 5}
```

Tell the user to open `verification_url` in a browser and enter `user_code`. Then poll:

```bash
curl -s -X POST https://api.danubeai.com/v1/auth/device/token \
  -H "Content-Type: application/json" -d '{"device_code": "DEVICE_CODE_FROM_ABOVE"}'
```

`428` = not approved yet (poll every `interval` seconds) · `200` = `{"api_key": "…"}` · `410` = expired, start over.

### 2a. Connect over MCP (recommended)

Danube's MCP server speaks streamable HTTP. For most MCP clients:

```json
{
  "mcpServers": {
    "danube": {
      "url": "https://mcp.danubeai.com/mcp",
      "headers": {
        "danube-api-key": "YOUR_API_KEY"
      }
    }
  }
}
```

The server also accepts `Authorization: Bearer YOUR_API_KEY`, and supports MCP OAuth for clients that prefer an authorization flow. Client-specific guides (Claude Desktop, Claude Code, Cursor, Windsurf, Codex, Hermes Agent, OpenClaw, and more): https://docs.danubeai.com/quickstart — OpenClaw users, see https://docs.danubeai.com/sdk/openclaw.

To keep the tool list small, append `?tools=core` to the URL, or a comma list such as
`?tools=core,workflows`; without it all 33 tools are described to the model. The six groups are
`core` (11 — `get_context`, discovery, execution, credentials, and `report_tool`), `workflows` (6),
`wallet` (6), `skills` (5), `feedback` (4 — `submit_rating`, `get_my_rating`, `get_tool_ratings`,
`get_recommendations`) and `context` (1 — `update_context`). `core` is added to whatever you ask
for, so search, execute and `get_context` can't be filtered out by accident. Unrecognised names are dropped silently: a typo alongside a valid group
just gives you fewer tools than you asked for, and if *none* of the names is a real group the
parameter is ignored altogether and you get the full list. Either way it fails quietly, never with
an error.

### 2b. Or use plain `curl` — no MCP needed

Every capability below is also a REST call against `https://api.danubeai.com/v1` with the header `danube-api-key: $DANUBE_API_KEY`. Copy-paste recipes: `references/rest-api.md` in this skill package; full reference: https://docs.danubeai.com/api-reference/introduction.

## Working with tools

Every task follows **Explore → Inspect → (Confirm) → Execute → Report.**

| Goal | MCP tool | REST |
|---|---|---|
| See what services exist | `list_services(query, limit)` | `GET /services` |
| Find a tool for a task | `search_tools(query, service_id?, limit?)` | `GET /tools/search?query=…` |
| All tools of one service | `get_service_tools(service_id)` | `GET /services/{id}/tools` |
| Full schema of one tool | `describe_tool(tool_id)`; the listings above return a summary (each parameter's type and whether it is required, no descriptions) unless you pass `detail="full"` | `GET /tools/{tool_id}` |
| Run a tool | `execute_tool(tool_id, parameters, fields?)` | `POST /tools/call/{tool_id}` with `{"tool_input": {…}}` |
| Run up to 10 at once | `batch_execute_tools(calls)` | `POST /tools/call/batch` |
| Inspect one tool before first use | `describe_tool(tool_id)` | `GET /tools/{tool_id}/describe` |
| Read a stored result back | `fetch_result(execution_id, path?)` | `GET /tools/executions/{execution_id}/result?path=…` |
| Remember a per-service default | `set_parameter_defaults(service_id, defaults)` | `PUT /services/{service_id}/parameter-defaults` |

### Reading what comes back

`execute_tool` returns `{result, _meta}` once a call actually reaches a tool. When the tool
returned JSON, the already-parsed object is at **`result.data`**. Read that;
`result.content[0].text` then holds only a pointer to it, so the payload is not sent twice.
A text-only result stays in `result.content[0].text`. Don't index `_meta`
blindly: it is absent when the lookup itself failed (no `tool_id` or `tool_name` given, or
neither resolves).

Oversized payloads are trimmed. A JSON array is trimmed at element boundaries, so it stays
parseable; plain text is cut mid-string with an inline `... [truncated - response was N
chars, …]` note; and a JSON object with no array big enough to trim degrades to
`{"_truncated_partial_json": "<prefix>", "_truncated": true}` — valid JSON, but the
object's own keys are gone, so re-request it rather than parsing that. Check
`_meta.truncated`, and `_meta.truncation_note` for what went. To get the rest, raise
`max_response_chars` (default 50000, hard max 500000) or follow `_meta.cursor` when the
upstream paginates. When a tool is resolved by *name*, a publisher's lower per-tool default
can apply, so trimming below 50000 is normal there. **Inside `batch_execute_tools` the
per-element default is lower — 32000 over MCP, since a batch multiplies it by up to ten** — so
a payload that survives a single call can still be trimmed as one element of a batch. (Raw
REST has no such batch default; an entry with no `max_response_chars` gets 500000.) Truncated
≠ broken — don't `report_tool` it.

**A slow MCP-backed tool can be given more time.** `execute_tool` takes `timeout_seconds` for a
single call — 1 to 300, defaulting to about 30 — and `POST /tools/call/{tool_id}` accepts the
same key beside `tool_input`. It reaches only tools served by an MCP server (an Apify actor
run, a crawl, a report rendered server-side are the cases worth raising it for). **On an `api`
service it is accepted and then ignored**: an HTTP tool's budget comes from its own catalog row
(30 s unless the publisher set otherwise), which a caller cannot extend — so if one of those
keeps timing out, the fix is a narrower request or a `report_tool`, not a bigger number. Past
300 seconds the work belongs in an asynchronous flow either way: start it, keep the handle it
returns, and poll with that service's own status tool.

**Credential values are masked before you ever see them.** Every tool response passes
through Danube's redactor on the way out, and unless the tool is one specifically declared
as returning a credential (see below), anything that reads as a secret — API keys, tokens,
passwords, connection strings — comes back as `[REDACTED:<FIELD>]`. That is
platform policy, not a broken tool: don't `report_tool` it, and don't ask the tool to
return the raw value. When there is something to report, the redaction object carries an
exact `count` plus a `fields` list — the first 50 masked values, each with its `path`,
`key`, and the `rule` that caught it — and a `mode`. It lives at `_meta.redaction` over
MCP, where the key is absent when nothing was masked, and at top-level `redaction` over
REST, where the key is always present and simply `null`. Either way, test the value, not
the key. Pagination cursors and identifiers are exempt by name, so paging keeps working.

A tool whose whole *purpose* is to mint a credential is not redacted into uselessness.
Where Danube can, it captures the secret for you: the new value is stored as the user's
credential for the target service and `_meta.redaction.stored_credentials` says where it
landed, so the service's later calls pick it up through ordinary credential injection and
you never hold the value.

**Where it can't, the secret reaches you raw** — a tool marked as returning a credential
with no capture mapping, a self-hosted deployment (custody stays in the customer's own
environment), or a store that failed. These come back completely unmasked with
`_meta.redaction.passthrough_reason` explaining why, alongside `count: 0`. This is
not limited to short-lived handoff tokens like a Plaid *link* token: a Plaid **access**
token, a rotated webhook signing secret, and other durable secrets pass through this way.
Treat anything that arrives unmasked as a live secret — show the user where to put it,
don't echo it back, and don't paste it into a later call's parameters.

Discovery tips:
- Every search result carries `readiness`: `ready` means you can execute it now, `needs_credential` means the service needs a credential this account has not connected (send the user to `configuration_url`), `unavailable` means the service is retired. Ready tools are favoured: a tool that needs setup yields a few places to ready ones, so a clearly better match still comes first. Prefer a ready tool over a similar one that needs setup.
- Every result also carries `deprecated`, `deprecation_message` and `sunset_date`. A deprecated tool is still listed and still runs while its sunset date is absent or in the future — that is an advance warning, and the message usually names what to use instead, so read it and switch rather than reporting the tool. Once the date has passed the tool disappears from search altogether, and `execute_tool` refuses it before calling anything — the message reads "… was deprecated and passed its sunset date (DATE): … No call was made." Both transports now name it outright: `error_type` is `tool_sunset` with `fault: "danube"` — top-level over REST, in `_meta` over MCP. (Until 2026-09-10 the value was the upper-case `TOOL_SUNSET` and the MCP layer passed no field at all, so keep matching the message text too if you may be talking to an older backend.) Search again for the replacement; nothing about it is retryable.
- Describe the *outcome* in natural language (`"send an email"`, `"create a GitHub issue"`, `"translate text"`); search is semantic.
- **But when you already know a tool's exact name, search for exactly that.** A tool whose name or slug matches the query exactly is pinned first, ahead of the readiness and reliability re-orderings, so `results[0]` is the tool you named. Every result now carries a `slug` (`stripe-api-read`) beside its id, and a slug is accepted anywhere a tool id is — `execute_tool`, `describe_tool`, `GET /tools/{id}`. Prefer it when showing the user what you're about to run; it is far more readable than a UUID. It is not a reason to skip searching, though: resolve the tool in this session rather than replaying an identifier from memory.
- Sample-data services (JSONPlaceholder, DummyJSON) are ranked behind every real result unless your query names one of them. They need no credential, so they would otherwise outrank the real tools of a service the user hasn't connected yet. If one turns up for a generic query, treat it as what it is — a fake-data sandbox, not an integration to offer the user.
- If the first query misses, rephrase and try again before concluding a capability doesn't exist. The catalog is far larger than you'd guess.
- `list_services` is served from a cache refreshed every 60 seconds, so a service connected or published moments ago can be missing from it for up to a minute; `search_tools` and `get_service_tools(service_id)` are not cached. Don't conclude a service doesn't exist from one `list_services` miss; search for one of its tools instead.
- Read the tool's parameter schema (`required`, `type`, `enum`, `tips`) before executing; ask the user for anything required that you don't have. Never reuse tool IDs from memory — search again.
- Pass only parameters the schema declares. On the standard HTTP execution path, Danube drops anything the tool row doesn't declare *before* making the upstream request, so an invented parameter never reaches the provider. If such a call then fails, the error message ends with a note naming what was dropped — "`'x'` was not sent — `<tool>` does not declare that parameter, so Danube dropped it before making the request. If the upstream supports it, the tool's schema is missing it." Read that note rather than concluding the upstream ignored your value, and treat it as a sign the tool's schema may be missing a parameter the provider does support. Tools backed by an MCP server forward your parameters as given, so there the upstream really did see them.
- Results called in the last 30 days also carry `reliability`: `window_days`, `calls_7d`, `calls_30d`, `success_rate_30d`, `p50_seconds`, `p95_seconds`, `last_success_at`, `last_failure_at`, `last_error_class`, `last_error_type`, an `error_classes` breakdown, a `faults` breakdown, `auth_failures_30d`, `caller_failures_30d`, `danube_failures_30d`, `upstream_failures_30d`, `distinct_callers_30d`, `flagged` and `refreshed_at`. When two tools fit, prefer the higher success rate; a `flagged` tool (under 80% success on 20+ calls) is a last resort, and say so to the user. `success_rate_30d` is `null` under 10 calls — that is too small a sample to rank on, not a bad tool. Results are relevance-ordered with a bounded reliability nudge; pass `ready_only=true` to see only tools you can call now, or `min_success_rate=0.9` to drop tools with a known-poor 30-day rate.
- **The score counts only what the tool is answerable for.** Since 2026-09-10 the sample behind `calls_30d` and `success_rate_30d` is successes plus `upstream` failures — the third party's own. Failures attributed to the *caller* (a missing credential, values the upstream correctly rejected) or to *Danube* (a stale row, a request Danube built wrong) are reported beside the rate and left out of it, so neither can push a tool over the `flagged` line. `faults` breaks the 30-day failures down by `caller` / `upstream` / `danube`, and the three `*_failures_30d` counters give the same thing as totals. A large `danube_failures_30d` is a Danube-side bug already queued for repair, not a reason to steer the user off a working tool; a large `caller_failures_30d` (of which `auth_failures_30d` is the credential part) is other callers' setup, not the tool. This generalises the older rule that only `auth` failures were excluded.
- `last_error_type` is the execution engine's own verdict (the `error_type` vocabulary below) on the most recent **upstream** failure — not on the most recent failure of any kind. `last_failure_at` and `last_error_class` are drawn from that same upstream-only row, which is why a caller-side failure minutes ago leaves all three untouched: they describe how the *provider* last let this tool down. `last_error_type` is additionally `null` for failures recorded before 2026-09-10, so `last_error_class` is the one populated on older rows.
- Read `last_error_class` before writing a tool off: `validation`, `upstream_4xx`, `upstream_5xx`, `timeout` or `other` (null when the tool has no counted failure). Upstream throttling has no class of its own — a `429` lands in `upstream_4xx` or `upstream_5xx` depending on how the provider worded it, so neither one is proof of a server fault. `auth` — a caller's missing or invalid credential — never appears here by design; it shows up only in the counters — `error_classes`, `auth_failures_30d`, and the wider `caller_failures_30d` / `faults` that now contain it — and those failures are left out of `calls_30d` and `success_rate_30d` entirely, so they can't push a tool over the `flagged` line. A tool with a large `auth_failures_30d` is one other callers haven't connected, not a broken one: if your account has the service connected, judge it on the rate.
- Before a tool you have not used successfully in this session, call `describe_tool(tool_id)` (REST: `GET /tools/{tool_id}/describe`): it returns the schema, your readiness, the last 30 days of usage with the most common errors, and your own last working parameter sets.
- If a service always needs the same identifier (Vercel `teamId`, a Sentry org slug, a DigitalOcean app id), save it with `set_parameter_defaults(service_id, {"teamId": "…"})` as soon as a call using it has succeeded, without asking first, and tell the user in one line what you saved; they see it on the service's page in the Danube dashboard and can remove it there. Later calls that omit it get it filled in (`_meta.defaults_applied` says which). Don't save one-off values such as a single record id, and never save a key or token this way.
- Keep big results out of your context: pass `fields: ["items[].name", "pagination.next"]` to `execute_tool` to receive only those paths. The result is stored under `_meta.execution_id`; `fetch_result(execution_id, path)` (REST: `GET /tools/executions/{id}/result?path=…`) reads any other part of it back without re-running the tool.
- **A batch entry takes `fields` too, and since 2026-09-10 it is honoured** — before that it was accepted and silently dropped. Put it beside `tool_input` on any element of `batch_execute_tools` (REST: any entry of `POST /tools/call/batch`). Each element then carries its own projection report and execution handle: `_meta.projection` / `_meta.execution_id` over MCP, `results[i].result.projection` / `.execution_id` over REST. **The two transports trim in opposite orders, so the advice differs.** Over MCP prefer `fields` to a bigger `max_response_chars`: the 32000-char per-element cap is applied *after* the backend has projected and stored, so a projection keeps a verbose payload whole, and `fetch_result` on the element's handle reads back everything the backend kept — which is up to its own 500000-char shaping limit, since the MCP layer never forwards your cap to it. Over REST an entry's `max_response_chars` (default 500000 there, not 32000) trims *before* the result is projected and before it is stored — so there a narrower request is the only real lever, and fetching a trimmed element's handle back does not recover what the cap cut. Over MCP the element's payload also sits at `result` itself, with no `data` wrapper (over REST that slot holds the whole envelope, with the payload one level further in) — but either way the paths are written against the same shape you would use on the single call.
- **The stored copy has its own ceiling: 1,000,000 serialised characters** — but it is rarely the ceiling that binds, because what gets stored is what the response *shaping* already produced. Over MCP your `max_response_chars` never reaches the backend at all (the MCP layer applies its own cap afterwards), so the stored copy is shaped at the backend's 500000 default and can't exceed it; the 1,000,000 ceiling only bites over REST, and only when a call asks for more than that. Past whichever limit applies first, a JSON result loses whole array elements from the tail (still valid JSON of the same shape, just short) and a plain-text one is simply cut at the limit. The REST handle then says so with `stored_truncated: true` and a `stored_meta` block giving the original size and what was dropped; `fetch_result` over MCP carries **neither** key, so there an oversized handle is quietly partial. If you are paging a very large result by path and the tail is missing, re-run the tool with a narrower request rather than concluding the upstream had nothing more.
- **A path that matched nothing is reported, not silently empty.** Write `fields` and `path` against the shape you saw in `result.data`, and when a projection comes back empty check the flag before concluding the tool returned nothing: on `execute_tool`, `_meta.projection.missing` lists the paths that hit nothing and `_meta.projection.matched_nothing` is true when none of them matched; on `fetch_result`, `_meta.path_matched` is `false` and the text block says so outright ("Nothing in the stored result matched the path … The result itself is intact"). REST reports the same thing as `path_matched` in the `GET /tools/executions/{id}/result` body. A correct path over an empty collection is not a miss: it comes back as that empty list and is listed in `_meta.projection.empty` (on `fetch_result`, `path_matched: true` plus `_meta.path_empty: true`). So a `false` means the path is wrong; re-read the original shape rather than re-running the tool. The key is *absent*, not `false`, when the backend doesn't report it, so test it as `_meta.get("path_matched") is False`.
- **A path may continue into a string that carries JSON.** MCP-backed tools routinely hand back their real payload as text inside a string, and a path written straight through it used to match nothing. It is now resolved against the JSON document the string carries — either the string that *is* JSON, or JSON fenced in the `<untrusted-data-…>` markers Supabase's MCP server wraps results in — and the fragment comes back parsed, in place of the string. So write `fields` against the shape you can see in the payload and don't route around a string in the middle of it. A path *ending* at a string still returns the string unchanged, and prose with a JSON fragment loose somewhere inside it is still a miss. Since 2026-09-10 the step `<json>` is also accepted and skipped as an interior step, which means a path read straight off `_meta.redaction` — that report spells the same place as `content[0].text.<json>.deployment.phase` — can be pasted into `fields` or `path` verbatim instead of being hand-edited. (Before that date the marker was matched as a literal key and the path silently hit nothing.) The trade is that a real key spelled `<json>` is unreachable; no upstream is known to have one.

### When a call fails, read whose failure it was

A failed `execute_tool` is attributed for you, so you don't have to guess from prose whether
the provider is down, your parameters were wrong, or Danube's own catalog row is stale. Three
fields carry it — `_meta.error_type`, `_meta.fault` and `_meta.retryable` over MCP, the same
three at the top level of the response over REST:

| `fault` | What it means | What to do |
|---|---|---|
| `caller` | The request itself was rejected: a missing or invalid credential, a value the provider correctly refused, a resource you named that doesn't exist, a plan or spending cap | Fix the parameters or credentials before retrying. Not the tool's fault — don't `report_tool` it |
| `upstream` | The third party failed: a 5xx, a timeout, a refused connection, throttling, an MCP server that couldn't be reached | Retrying later may work. This is the only class that moves the tool's reliability score |
| `danube` | Danube's own handling: a stale catalog row, a request it built wrong, a credential it couldn't resolve, an internal exception | Neither a retry nor a `report_tool` helps — a genuine defect here is recorded for repair on its own, and the rest (a sunset tool, a retired service, a disabled tool) are deliberate catalog states, not bugs to chase. Pick another tool or tell the user |

`error_type` is the specific class inside that verdict (`missing_parameters`, `auth_required`,
`rate_limited`, `timeout`, `endpoint_missing`, `upstream_tool_missing`, `bad_request`,
`not_found`, …), and `retryable` says whether the *identical* call could succeed later — true
only for the genuinely transient upstream types. Prefer these three fields to matching on the
message text, which is what the rest of this file falls back to for older backends.

**Over MCP the error text usually repeats the verdict**, opening with `Request error:`,
`<Service> error:` (the service is taken from the tool name's `<Service> - <Action>` shape, and
falls back to `Upstream service error` when the name doesn't have it) or `Danube error:`, then
a sentence of advice. Treat that as a convenience for showing the user, not something to match
on: two of the most common failures — `auth_required` and an insufficient wallet balance —
return their own dedicated text with no prefix at all, and over REST nothing is prefixed. The
three fields are the reliable signal.

Don't confuse `error_type` with the `last_error_class` in a search result's `reliability`
block: they are separate vocabularies. `error_type` is the execution engine's verdict on one
call; `last_error_class` is the reliability view's coarser after-the-fact bucket
(`validation`, `upstream_4xx`, `upstream_5xx`, `timeout`, `other`).

Inside `batch_execute_tools` the same three fields ride on each *element's* own `_meta`, not on
a top-level one — read them per element.

They can still be absent. A failure that never reached the execution engine has none: an
unresolvable `tool_id`, or, over MCP, a `tool_name` that matches nothing (which returns no
`_meta` at all). And a refusal the REST layer turns into a real HTTP status — `402` for a plan
cap, `503` for a disabled tool — *was* attributed, but the status is raised in place of the
body, so the fields don't survive to the caller. Read all three with `.get()` and fall back to
the message whenever they're missing.

### When a tool needs credentials

A result containing `"error_type": "auth_required"` (with `fault: "caller"`) means this user hasn't connected that service yet:
1. Prefer sending the user to the `configuration_url` in the error (or https://danubeai.com/dashboard) to connect it — the only path for OAuth services such as Gmail, Slack, or Google Calendar.
2. For API-key services, if the user explicitly gives you the key and confirms, call `store_credential(service_id, credential_type="bearer", credential_value=…)`, then retry.
3. If `auth_required.reason` is `"org_policy"` with `credential_policy.mode: "shared"`, the user's organization requires its own shared credential for that service and hasn't added it. Don't ask for a personal key, because it would be ignored. Tell the user an owner or admin of the named organization needs to add it.

`get_service_tools` reports `needs_configuration: true` only when *every* tool of the service is `needs_credential` for this account; it is derived from the per-tool `readiness` field, which is the answer for any single tool. A service mixing credential-free and credential-bearing tools reports `false`, so read `readiness` on the tool you are about to call rather than the service-level flag.

When it *is* true, read the `configuration_required` block instead of guessing what to ask for. The keys are always present: `service_id`, `service_name`, `message`, `configuration_url`, `instructions` and `credential_schema`. `service_name` can still be null (the lookup behind it is best-effort) — fall back to the service you asked about rather than showing the user "null". For a service whose credential shape Danube knows, it also carries a `credential_schema` — the field names, labels, help text and any eligibility notes — and the `instructions` then name those fields ("Ask the user for: …"). `credential_schema` stays `null` for a service with no registered schema, which is common for plain API-key services; there, ask for the one key or send the user to `configuration_url`. Don't invent field names it doesn't list.

`credential_value` takes either the raw secret or a **reference** — a pointer resolved when the tool runs, so the secret is never held centrally.

**References only work on some execution paths.** A reference is resolved on the standard HTTP auth-injection path — an `api` service whose tool isn't one of the provider-specific composite handlers — and by the in-VPC data-plane agent, which runs the services an org has marked `local_only`. Everything else reads the stored string **as the secret**: `mcp_server` services, and composite handlers for services like Firecrawl or Notion. Give one of those a `vault://…` string and it goes out as the credential itself, and the provider answers `401`. So: **store a reference only for a self-hosted deployment, or for a service that runs on the org's data-plane agent; otherwise store the value.** If you aren't sure which path a service takes, store the value.

| `credential_value` | Stored as | Resolves |
|---|---|---|
| `vault://<mount>/<path>#<field>` (the `#<field>` is required) | reference | against the `VAULT_ADDR` / `VAULT_TOKEN` of whatever process runs the tool — your data-plane agent or self-hosted deployment. The hosted service has no per-customer Vault setting, so it can't reach your Vault |
| `env://VAR_NAME` | reference | in the environment where the tool actually runs, so self-hosted / data-plane only. On the hosted service `env://` stores fine but **fails at execution time**: it is off by design there (`DANUBE_ALLOW_ENV_REFERENCES` is a self-hosted/dev switch, not something to ask Danube to enable) |
| the raw key | value | the working default on the hosted service — deprecated but accepted; self-hosted rejects it outright with `central_credential_ingestion_disabled` |

Never invent a variable name or vault path — use only what the user gave you, and ask which form they want if it isn't obvious. A successful store echoes `"stored_as": "reference"` or `"value"`, adding `"deprecated": true` and a `notice` when a plaintext value was stored. Note that a successful store proves nothing about whether the reference will resolve — that only shows up when the tool runs.

HTTP `429` with an `upgrade_url` means the account's plan limit is reached — tell the user; don't retry in a loop.

`error_type: "upstream_tool_missing"` is the one failure that is **Danube's** fault rather than
yours or the provider's: the catalog row points at a tool its MCP server no longer offers. (It is
a body-shaped error, not an HTTP status — over REST the call still answers `200` with
`status: "error"`; over MCP it arrives as `isError: true` with the fields below in `_meta`.) Danube tries to repair it on the spot first — it re-reads the server's tool list and, if
the tool was merely renamed, re-runs your call under the new name and hands you the real result,
so most drift never reaches you at all. What you see is the case where the tool is genuinely
gone. The response carries `retryable: false`, the `upstream_tool` the row pointed at, and
`available_tools` — the tools the server *does* offer. Take the answer at face value: it has
already been reported automatically, so don't `report_tool` it and don't retry. Pick a
replacement from `available_tools`, or search again.

Danube's own AI tools (`Danube - Summarize Text`, `Translate Text`, `Webpage Summary`, and the
six others that call a model — not `Danube - Screenshot`) carry a **separate daily cap on a free
account: 20 successful executions per UTC day**, on direct `execute_tool` calls as well as on
workflow steps. Past it the call is refused with "Daily limit for Danube's AI tools reached
(N/20 today on the free plan). It resets at 00:00 UTC…" — over REST an HTTP `402` whose
`error.code` is `payment_required`, over MCP a result with `isError: true` quoting the same
message in `content[0].text`. Tripping it *inside* a workflow is not a 402 at all: that step
simply fails with the message and the run comes back `200` with `status: "failed"`. It resets,
so it is not the lifetime workflow cap below; either wait or tell the user to upgrade, and
don't `report_tool` it.

### Tools the user's organization registered

Alongside the catalog, an organization can register its own services with visibility `internal` (Danube runs the calls) or `local_only` (the organization's in-network data-plane agent runs them). They show up in `search_tools` and `list_services` only for members of that organization, and only for the tools the organization's policies grant this user; everyone else gets a `404`, the same as a tool that does not exist. Which organization you act in is set by the key: a key created for an organization acts for it on its own, and a `danube-org-id` header (an organization id the user belongs to) chooses one explicitly and wins over the key. Four things to read:

- **`403` with `Your organization does not have access to this tool`** is the organization's policy, not the key's `allowed_tools`. Don't retry and don't `report_tool` it: tell the user an admin of the organization has to grant the tool (or its service) to them, a team they are in, or their role. A public catalog tool can be refused this way too, when the organization you act in explicitly denies it; that deny does not follow you outside the organization's context. A tool the policy withholds is normally hidden from search, so this mostly appears on an id you were holding.
- **Generic tools on a connected system.** A system an admin connected from one URL carries a small governed surface next to any named tools. For an API it is `<Service> - Request` (parameters `method`, `path`, `query`, `body`, `headers`; the credential is injected for you, so never put one in `headers`). For a database it is `<Service> - List tables`, `<Service> - Describe table` and `<Service> - Query` (one read-only `SELECT` in `sql`). Prefer a named tool when one fits. Otherwise call **List tables** and **Describe table** before **Query**, so the statement names real tables and columns, and keep a Request to `GET` or `HEAD` unless the user asked for a write. The service's access policy decides what these tools may reach: which methods and path prefixes, which tables, which columns come back masked as `[REDACTED:<column>]`, and the row cap.
- **`error_type: permission_denied` with `fault: caller` on one of those tools is the policy, not a bug.** The message names the refused method, path or table. Don't retry the same call, don't look for another path to the same data, and don't `report_tool` it: tell the user what was refused and that an admin can widen the policy on the service page, or promote the call as a named tool. A write (`POST`, `PUT`, `PATCH`, `DELETE`) may instead come back as `confirmation_required` when the policy asks for consent: show the user the call and repeat it with the token, like any other confirmed call.
- **Connecting or registering a system** is an owner/admin action. `POST /organizations/{org_id}/connect` takes one input (`kind: "api"` with `api.base_url`, `kind: "database"` with `database.connection_ref`, `kind: "mcp"` with `mcp.server_url`) and `"dry_run": true` first, which discovers and previews without writing anything; `POST /organizations/{org_id}/services` with `source` (`openapi_url`, `openapi_spec`, `mcp` or `connector`) is the advanced path for a document or spec the user already holds. Confirm with the user before the real write, like any other. Guide: https://docs.danubeai.com/organizations/internal-tools

## Beyond tools

Available through the same connection, same guardrails:
- **Skills:** `search_skills`, `get_skill`, `create_skill`, `update_skill`, `delete_skill`. Search covers public skills, the user's organizations' internal skills and their own private ones, the last two first; `visibility` (`public`, `internal`, `private`) and `org_name` say which. The skills `get_context` lists are the ones meant for this user: load one with `get_skill` when a task calls for it. `get_skill` returns SKILL.md, every text file with its `path` in the skill folder, and a `download_url` (the whole skill as a zip ready for `.claude/skills/`, fetched with the `danube-api-key` header; binary files are only in the zip). `create_skill` takes `visibility` `private` or `internal` (every member of the organization, `org_id` defaulting to the one the connection acts for); an organization can limit publishing to its owners and admins.
- **Workflows:** `list_workflows`, `create_workflow`, `update_workflow`, `delete_workflow`, `execute_workflow`, `get_workflow_execution`
  - A workflow can outlive the call that started it. `execute_workflow` answering
    `status: "running"` with an `execution_id` means it is **still going, not failed** —
    poll `get_workflow_execution(execution_id)`. Never re-run it: a second concurrent run
    duplicates every write the first one makes. Over `curl` you get a plain timeout rather
    than that response, so treat *any* timeout on an execute call the same way.
  - **A failed run is not an error status.** A workflow whose steps fail still answers
    normally, with `status: "failed"` in the body. Read the body's `status` and the
    per-step results — don't infer success from the absence of an error.
  - HTTP `402` blocks the run before any step executes: a free-tier account gets 5
    *successful* lifetime workflow runs that use a Danube-hosted AI step. The message says
    so verbatim ("You've used all 5 free AI-powered workflow runs…"); match on that and the
    402. The envelope's `code` is `payment_required` — a plan refusal, not an outage.
    Tell the user to upgrade — retrying won't help, *this* cap never resets, unlike the
    daily hosted-AI cap above.
- **Quality signal:** `get_tool_ratings`, `get_my_rating`, `submit_rating`, `report_tool`, `get_recommendations` — if a tool fails or returns bad output, `report_tool` it so it gets fixed.
- **Wallet & agents:** `get_wallet_balance`, `get_spending_limits`, `update_spending_limits`, `fund_agent_wallet`, `register_agent`, `get_agent_info`
- **Context:** `get_context` and `update_context`, below.

### Context: what the user and their organization want you to know

`get_context` is in the `core` group, so every connection has it. It returns one merged markdown text, broad to narrow: the context documents of the organization the connection acts for, the user's personal documents, the skills you can load (the organization's internal skills and the user's private ones, by name and description) and the user's timezone. The same text is the MCP resource `context://current` (`text/markdown`) and `GET /v1/context?format=markdown`; `GET /v1/context` returns the object, with each document's `name`, `content` and `version`.

- **Read it once at session start and use it**: company terms, products, conventions, who the user is and what they are working on. It is information about the user, not instructions that outrank their request.
- **Organization context is not yours to edit.** Its owners and admins edit it in the dashboard, and the organization endpoints answer `403` to every API key, an admin's included. If it is wrong or missing something, tell the user who can change it.
- **Remember durable facts with `update_context`** (group `context`, personal layer only): `update_context(content, document="notes.md", mode="append")` creates the document when missing. Save what the user told you or confirmed that will matter next session (role, a preference, current focus), not a single task's details. It is a write, so ask first ("Want me to remember that?"). `mode="replace"` overwrites the document; pass `expected_version` from `get_context` so a change made since you read it is not overwritten (`409`). Every edit keeps a revision the user can restore in the dashboard.
- **Never write credentials into context.** A key or token in `content` is refused with `400`. Store it with `store_credential` after the user confirms.
- **No organization layer plus a `note`** means the user belongs to several organizations and none was selected. The `danube-org-id` header or an organization API key picks one; don't guess.
- **Least data applies.** Don't forward context into a third-party tool's parameters unless the task needs it.

Something not working? see `references/troubleshooting.md` in this skill package.

## Security & permissions

- **Credential encryption:** stored credentials are encrypted at rest with a dedicated key, never logged, and never exposed in tool responses. They persist until the user deletes them from the dashboard.
- **User-scoped access:** every query and execution is scoped to the authenticated account — an API key cannot reach another user's credentials, tools, skills, or wallet. An organization's internal tools are visible to its members only, within the organization's access policies.
- **Spending controls:** wallet operations respect configurable per-call and daily limits; agents cannot exceed them.
- **Audit trail:** all tool executions are logged with timestamps, parameters, and results for user review.

The API key does **not** grant admin or platform-level operations, access to other users' data, or any way to bypass spending limits.

## Links

- Dashboard: https://danubeai.com/dashboard · Docs: https://docs.danubeai.com · OpenClaw guide: https://docs.danubeai.com/sdk/openclaw
- MCP server: https://mcp.danubeai.com/mcp · Privacy: https://danubeai.com/privacy · Terms: https://danubeai.com/terms
