# Recipe: let your board use a service while it works

A connection lets a board use one of your own outside services in the middle of work: an agent asks for a video, a column step makes one when a card arrives, or a person asks on the card. BakedBrie makes the call with your key, puts the result file on the card with a receipt, and starts the agent again with the result. The finished work still goes through the maker, the checker and a person's approval before anything leaves.

How it works:

- A **connection** holds the service's base URL, how the key is sent, where result files may come from, and the only **operations** (call shapes) BakedBrie may make. Nothing else can ever be called: an agent or a card fills in input values, never the host, the path, the model or a header.
- An **attachment** allows a connection on one board, with a handle (the name agents use, such as `acme-video`), the operations, the agents and columns that may ask, and who may use it from a card.
- **Limits** start at zero. Only a person in the BakedBrie web app can set or raise them. Until they do, nothing that can cost money runs.
- The key goes only to the service's base URL, in its auth header. It never appears in the chat, a preview, an answer, a receipt or an agent's prompt.
- A call is sent at most once. If BakedBrie cannot tell whether the service accepted it, it says so and never sends it again by itself.
- Your account at the service pays. BakedBrie never bills or marks up provider usage.

Every definition field is in [Concepts: Connections](/docs/concepts#connections). For fal video, [Make a fal video on request](/docs/recipes/connections-fal-video) has the whole definition ready to copy.

## Before you run it

1. Connect [Claude Code](/docs/connect-claude-code) or [Codex](/docs/connect-codex) to BakedBrie. `whoami` must show `connections` on (and `secrets`, for `prepare_secret`).
2. Put the service's API key in an environment variable in your own shell, for example `export ACME_MEDIA_KEY=...`. Never paste a key into the chat or put it on a command line.
3. Have these ready, from the service's own API docs:
   - the base URL (https), for example `https://api.acme-media.test`;
   - the header the key goes in and the text before it (`Authorization` and `Bearer `, or `Authorization` and `Key `, or `xi-api-key` and nothing);
   - the call: its path, its JSON body, and whether it answers at once with a file link (a single call) or with a job id you check on (an async job);
   - for a job: where the job id is in the answer, the path to check it, the status values for still working, done and failed, and where the file link is;
   - the host the result files are served from (for example `files.acme-media.test`);
   - optionally a harmless GET that proves the key works, for example `/v1/me`;
   - the price of one call, so the person can set the most one call can cost.
4. Fill in the UPPERCASE `{{...}}` values in the prompt. Lowercase ones like `{{card.brief}}` are BakedBrie templates: keep them as written.

## The prompt

```text
In BakedBrie, let the board "{{BOARD_NAME}}" use {{SERVICE_NAME}} while it works. Use only the BakedBrie MCP tools. Start with whoami and stop if connections is off. First read /docs/recipes/connections and /docs/concepts (section "Connections") with read_docs.

The API: base URL {{BASE_URL}}. The key is in my environment variable {{ENV_VAR}} and goes in the header {{HEADER}} as "{{PREFIX}}<key>". A harmless GET that proves the key works: {{CHECK_PATH}}. Result files come from {{OUTPUT_HOST}}.
The call: {{WHAT_THE_CALL_DOES_AND_ITS_DOCS}}. One call costs about {{PRICE}}.
Who may use it: the agent {{AGENT_NAME}} in the column {{AGENT_COLUMN}}, {{WHEN_TO_USE}}. {{OPTIONAL_COLUMN_STEP: "Also make the call when a card enters the column X, then move it to Y."}}

1. Find the board, its columns and agents with list_boards and read_board.
2. Call prepare_secret with kind {{SERVICE_KIND}}, purpose external_job and env_var {{ENV_VAR}}. Run the command it returns in my shell exactly as given, then use the drop_id. Never print the key.
3. Write the connection definition from the recipe's worked example. Call manage_connection with action preview and the definition (a draft), show me each request, and fix every problem it lists. A preview sends nothing.
4. When I agree, create it with manage_connection: action create, name "{{SERVICE_NAME}}", the definition, credential {"drop_id": "<the drop id>"}, and a new request_id.
5. Check the key: manage_connection action check_key, then key_check_status every few seconds until the state is done or failed. Tell me the HTTP status.
6. Attach it with manage_board_connection action attach: handle {{HANDLE}}, the operation, agent_ids, stage_ids, people and when_to_use.
7. If I asked for a column step, add it with manage_connection_step action create.
8. Stop and tell me: "Only a person can let a board spend. Open these links and set the limits", with ceiling_link and limits_link. Do not try to set limits yourself.
```

## The steps

### 1. Hand over the key with a drop

`prepare_secret` with `purpose` `external_job` returns a `command`. Run it in the user's shell exactly as given: it reads the variable and sends the value to BakedBrie, so the key never enters the chat or a command line. Pass the `drop_id` as `credential`. A drop lasts 10 minutes and works once.

### 2. Preview, then create

`manage_connection` action `preview` with a draft `definition`, an `operation_key`, sample `inputs` and, for file roles, `sample_files` (type and size only). It answers `requests` (each `kind`, `method`, `url`, `headers` and `body_preview`) and `problems`. The key shows as `•••• (saved key)` after its prefix, for example `Bearer •••• (saved key)`, and a file shows as `data:image/png;base64,… (1.8 MB)`. Nothing is sent. Fix every problem, then `create` with the same definition and the `credential`.

The create answer has `ceiling_link`: the web page where a person sets the connection's monthly ceiling (across all boards). You become the connection's custodian: only you (or a workspace owner or admin) can change it, check its key, attach it or remove it.

### 3. Check the key

`manage_connection` action `check_key` sends the one GET the definition's `key_check` names, in the background, and returns `key_check_id`. Read it with `key_check_status`: `state` becomes `done` (with `status_code`, for example 200) or `failed` (with `error_code`). Only the status is kept, never the answer.

### 4. Attach it to the board

`manage_board_connection` action `attach` with `board_id`, `connection_id`, `handle`, `operations`, and:

- `agent_ids`: the agents that may ask (none by default);
- `stage_ids`: the columns where they may ask (empty means every column);
- `people`: who may use it from a card: `board_workers` (default), `board_managers` or `nobody`;
- `when_to_use`: one plain sentence for the agents, such as "Only when the brief asks for a video."

You need to be the connection's custodian and have manage on the board. The answer has `limits.set: false` and `limits_link`.

### 5. A person sets the limits

Tell the user: "Only a person can let a board spend. Open this link and set the limits." and give them `limits_link` (and `ceiling_link`). In the web app they set, for this board: the most one call can cost (per operation), calls per card, calls per month and money per month; and for the connection, the monthly ceiling. An API token cannot set or raise them: it gets `CONNECTION_LIMITS_WEB_ONLY`. You can lower them with `lower_limits` and `lower_ceiling`.

### 6. Decide how work uses it

Any mix of three:

- **An agent asks.** Nothing more to set up: the attachment's agents are told about the connection in their instructions. Add a line to the maker's instructions when it helps, such as "If the brief asks for a video, ask acme-video for one from the card's still. When the file comes back, attach it."
- **A column step.** `manage_connection_step` action `create` with `board_id`, `board_connection_id`, `operation_key`, `when_stage_id` (a column that is not the first one and has no active agent rule), `then_stage_id` (where the card goes when the file is saved), `input_templates` (for example `{"prompt": "{{card.brief}}"}`) and `file_rule` (for example `{"pick": "latest_image", "role": "image"}`).
- **A person asks on a card.** In the web app, **Use a connection** on the card. Over MCP, `request_connection` (below).

## How agents ask

An agent allowed on this board and column sees the connection in its instructions: the handle, operation, inputs, the file it needs, the most one call can cost and how many calls this card has left. To ask, it ends its reply with exactly one block:

````
```bakedbrie-connection-request
{"connection":"acme-video","operation":"image_to_video","inputs":{"prompt":"Slow push in, soft morning light"},"files":{"image":"still.png"},"why":"The brief asks for a short video of this still."}
```
````

1. BakedBrie checks the handle, agent, column, operation, inputs, file and limits, reserves the most it can cost, and ends the agent's run with a working note. The card shows "Waiting on Acme Media".
2. It sends the call once, checks on the job, downloads the file, checks its type and size, and saves it on the card with a receipt.
3. It starts the same agent again. Its instructions now say the call finished and name the new file, for example `image_to_video-01a0eb-1.mp4`.
4. The agent writes its finished work and ends it with an attach block naming the file:

````
```bakedbrie-attach
{"files":["image_to_video-01a0eb-1.mp4"]}
```
````

5. The file goes to the checker and to review with the result, and reaches the outside world only after a person approves it.

Rules: one request per reply; a reply that asks must not also hold finished work, a `bakedbrie-outputs` block or an attach block; only makers ask (a checker's request is removed); inputs the board fixed cannot be sent; the same operation is not asked for twice in one round unless a person asked for changes since. A request that breaks a rule is refused with a code, and the agent gets one chance to correct it.

## How a column step runs

When a card enters the step's column, the step makes its call once for that round of work on the card. It fills the inputs from `input_templates` and picks the file with `file_rule`. When the file is saved, the card moves to `then_stage_id`, and that column's agent sees the new file listed under "Files BakedBrie made for this card", ready to attach. If the call does not finish well, the card stays in the step's column and shows why, with **Run again** for a person.

## Asking from a card over MCP

1. `request_connection` action `preview` with `card_id`, `board_connection_id`, `operation_key`, `inputs` and `files` (`{"<role>": "<card file id>"}`, from `list_card_files`). It checks everything, sends nothing, and answers `preview_id`, `preview_sha256`, the final `inputs` (with fixed values and defaults), the chosen `files`, `most_it_can_cost_micros` and `remaining` (`card_calls`, `month_calls`, `month_micros`, `ceiling_micros`). Anything that would stop the call (limits not set, a limit reached) is listed in `problems`.
2. Show the person the preview. When they agree, `request_connection` action `create` with `card_id`, `preview_id`, `preview_sha256` and a new `request_id`. **`request_id` is required**: without it the call is refused with `REQUEST_ID_REQUIRED`. A retry with the same `request_id`, or another create with the same preview, returns the same request and never calls twice.
3. `continue_with_agent` (default true) starts the agent on the card's column again with the result, so it can attach the file.
4. Follow it with `manage_connection_request` action `get` (or `list` for the card) until `state` is `done`.

Over the limit: when the card has used its calls, `preview` lists `CONNECTION_CARD_LIMIT` in `problems` and `create` is refused with `CONNECTION_CARD_LIMIT` ("This card has used all 2 calls to Acme Media."). Nothing is sent and nothing is counted. The same holds for `CONNECTION_MONTHLY_CALLS_LIMIT`, `CONNECTION_MONEY_LIMIT` and `CONNECTION_CEILING_LIMIT`.

## Receipts

Each request (`manage_connection_request` `get`, or `list` for a card) has a `state`, a plain `message` when something went wrong, and a `receipt`:

- `provider_job_id`: the service's own job id, to look it up at the service;
- `output_files`: each file saved on the card, with `card_file_id`, `name` (such as `image_to_video-01a0eb-1.mp4`), `sha256`, `bytes` and `media_type`;
- `counted_micros` and `cost_note`, `sent_at` and `finished_at`, the trigger (agent, person or step), the input files' names and hashes, and the definition's hash.

A receipt never holds a key, a link or a body.

## Unclear outcomes and Send again

If the call was sent but its answer never came back (a timeout, a dropped connection), BakedBrie cannot know whether the service accepted it. The request's state is `unknown` with `CONNECTION_OUTCOME_UNKNOWN`: it stays counted at the most it can cost, and **BakedBrie never sends it again by itself**.

What a person should do: check their account at the service. If the job is there, nothing more is needed. If it is not, a board manager can use **Send again** on the card in the web app (it may charge twice, so it is web only), or **Give up** (`manage_connection_request` action `give_up`). An agent never sends it again.

A call that has not finished in time (`timed_out`) can be checked again once (`check_again`).

## Limits and "most one call can cost"

- **Most one call can cost** is set per operation on the board. Every call reserves it before it is sent, and every limit is checked against it. BakedBrie never learns the real price, so it counts each call at this amount; the service may charge less.
- **Calls per card** counts over the card's whole life (default 2). **Calls per month** (default 20) and **money per month** are per board. The connection's **monthly ceiling** covers every board.
- Money in answers is micro-dollars: `100000` is $0.10. `manage_board_connection` action `usage` shows `counted_micros`, `counted_calls`, `money_left_micros` and `calls_left` for this month.
- Only a person in the web app sets or raises limits. Tokens and agents can lower them.

## Worked example: Acme Media

Acme Media is a made-up async video API (`https://api.acme-media.test`). It takes a still image and a prompt at `POST /v1/videos`, answers `{"id": "...", "state": "queued"}`, and `GET /v1/videos/{id}` answers `state` `queued`, `working`, `ready` or `failed`, with the file link at `file.url` on `files.acme-media.test` when ready. `GET /v1/me` proves the key. A video costs $0.10. Swap in your own service's values.

1. In your shell: `export ACME_MEDIA_KEY=...`
2. `prepare_secret`:

   ```json
   {"kind": "acme_media", "purpose": "external_job", "env_var": "ACME_MEDIA_KEY"}
   ```

   Run the `command` it returns, exactly as given. Keep the `drop_id`.
3. The definition:

   ```json
   {
     "base_url": "https://api.acme-media.test",
     "auth": {
       "header": "authorization",
       "prefix": "Bearer "
     },
     "fixed_headers": [],
     "key_check": {
       "path": "/v1/me"
     },
     "output_hosts": [
       "files.acme-media.test"
     ],
     "operations": [
       {
         "key": "image_to_video",
         "label": "Video from an image",
         "cost_hint": "Acme Media: $0.10 per video",
         "kind": "async_job",
         "inputs": [
           {
             "type": "text",
             "key": "prompt",
             "label": "What should move",
             "max_length": 1000
           },
           {
             "type": "integer",
             "key": "seconds",
             "label": "Length in seconds",
             "min": 2,
             "max": 10,
             "fixed": 5,
             "cost_affecting": true
           }
         ],
         "files": [
           {
             "role": "image",
             "label": "Still image",
             "media_types": [
               "image/png",
               "image/jpeg"
             ],
             "max_bytes": 4000000
           }
         ],
         "submit": {
           "method": "POST",
           "path": "/v1/videos",
           "body": {
             "prompt": "{{inputs.prompt}}",
             "seconds": "{{inputs.seconds}}",
             "image": "{{files.image}}"
           }
         },
         "job_id": "$.id",
         "status": {
           "path": "/v1/videos/{{job.id}}"
         },
         "result": "same_as_status",
         "status_values": {
           "field": "$.state",
           "pending": [
             "queued",
             "working"
           ],
           "succeeded": [
             "ready"
           ],
           "failed": [
             "failed"
           ],
           "error_field": "$.error"
         },
         "outputs": [
           {
             "path": "$.file.url",
             "media_types": [
               "video/mp4"
             ],
             "max_bytes": 45000000
           }
         ],
         "cancel": {
           "method": "POST",
           "path": "/v1/videos/{{job.id}}/cancel"
         },
         "poll": {
           "first_after_s": 2,
           "every_s": 2,
           "max_wait_s": 600
         }
       }
     ]
   }
   ```

   - `seconds` is `fixed` at 5 and `cost_affecting`: nobody can ask for a longer (costlier) video.
   - `{{files.image}}` becomes the card's still as `data:image/png;base64,...` when the call is sent.
   - The key goes only to `api.acme-media.test`; the video is downloaded from `files.acme-media.test` without it.
4. `manage_connection` with action `preview`, `definition` set to the JSON above, and:

   ```json
   {"action": "preview", "operation_key": "image_to_video", "inputs": {"prompt": "Slow push in"}, "sample_files": {"image": {"media_type": "image/png", "bytes": 1800000}}}
   ```

   The answer (trimmed) shows the submit, status and cancel requests. Nothing is sent:

   ```json
   {"requests": [{"kind": "submit", "method": "POST", "url": "https://api.acme-media.test/v1/videos", "headers": {"content-type": "application/json", "accept": "application/json", "authorization": "Bearer •••• (saved key)"}, "body_preview": "{\"prompt\":\"Slow push in\",\"seconds\":5,\"image\":\"data:image/png;base64,… (1.8 MB)\"}"}, {"kind": "status", "method": "GET", "url": "https://api.acme-media.test/v1/videos/{job id}", "headers": {"accept": "application/json", "authorization": "Bearer •••• (saved key)"}, "body_preview": null}], "problems": []}
   ```

5. `manage_connection` with action `create`, `definition` set to the same JSON, and:

   ```json
   {"action": "create", "request_id": "5b1f0e7a-3c1d-4f7e-9a51-2d6c0b9e8f10", "name": "Acme Media", "description": "Made-up video API", "credential": {"drop_id": "<the drop id>"}}
   ```

   The answer has the connection's `id`, `auth.has_key: true`, `monthly_ceiling_micros: null` and `ceiling_link`.
6. `manage_connection` with `{"action": "check_key", "connection_id": "<id>"}`, then `{"action": "key_check_status", "connection_id": "<id>", "key_check_id": "<key_check_id>"}` until `state` is `done` (`status_code` 200).
7. Attach it to the board "Video posts" for the maker in "Creating":

   ```json
   {"action": "attach", "board_id": "<board id>", "connection_id": "<id>", "handle": "acme-video", "operations": ["image_to_video"], "agent_ids": ["<maker agent id>"], "stage_ids": ["<Creating column id>"], "when_to_use": "Only when the brief asks for a video."}
   ```

   The answer has `limits.set: false` and `limits_link`. Tell the person: "Only a person can let a board spend. Open this link and set the limits." For example: most one call can cost $0.10, 2 calls per card, 10 calls per month, $1.00 per month, and a $1.00 monthly ceiling on the connection.
8. A column step on "Make video" that moves the card to "Creating" when the video is saved:

   ```json
   {"action": "create", "board_id": "<board id>", "board_connection_id": "<attachment id>", "operation_key": "image_to_video", "when_stage_id": "<Make video column id>", "then_stage_id": "<Creating column id>", "input_templates": {"prompt": "{{card.brief}}"}, "file_rule": {"pick": "latest_image", "role": "image"}}
   ```

9. After the limits are set, a card moved into "Make video" (`move_card`) makes one call. `manage_connection_request` action `list` with the card's id shows it going from `queued` to `running` to `done`, and the card moves to "Creating" with the video on it. Its receipt names the `provider_job_id` and the output file, such as `image_to_video-01a0eb-1.mp4`. `manage_board_connection` action `usage` then shows `counted_micros: 100000` and `money_left_micros: 900000`.
