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. For fal video, Make a fal video on request has the whole definition ready to copy.
Before you run it
- Connect Claude Code or Codex to BakedBrie.
whoamimust showconnectionson (andsecrets, forprepare_secret). - 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. - 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 (
AuthorizationandBearer, orAuthorizationandKey, orxi-api-keyand 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.
- the base URL (https), for example
- Fill in the UPPERCASE
{{...}}values in the prompt. Lowercase ones like{{card.brief}}are BakedBrie templates: keep them as written.
The prompt
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_managersornobody;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_stepactioncreatewithboard_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}}"}) andfile_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."}
```
- 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".
- 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.
- 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. - 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"]}
```
- 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
request_connectionactionpreviewwithcard_id,board_connection_id,operation_key,inputsandfiles({"<role>": "<card file id>"}, fromlist_card_files). It checks everything, sends nothing, and answerspreview_id,preview_sha256, the finalinputs(with fixed values and defaults), the chosenfiles,most_it_can_cost_microsandremaining(card_calls,month_calls,month_micros,ceiling_micros). Anything that would stop the call (limits not set, a limit reached) is listed inproblems.- Show the person the preview. When they agree,
request_connectionactioncreatewithcard_id,preview_id,preview_sha256and a newrequest_id. **request_idis required**: without it the call is refused withREQUEST_ID_REQUIRED. A retry with the samerequest_id, or another create with the same preview, returns the same request and never calls twice. continue_with_agent(default true) starts the agent on the card's column again with the result, so it can attach the file.- Follow it with
manage_connection_requestactionget(orlistfor the card) untilstateisdone.
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, withcard_file_id,name(such asimage_to_video-01a0eb-1.mp4),sha256,bytesandmedia_type;counted_microsandcost_note,sent_atandfinished_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:
100000is $0.10.manage_board_connectionactionusageshowscounted_micros,counted_calls,money_left_microsandcalls_leftfor 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.
- In your shell:
export ACME_MEDIA_KEY=... prepare_secret:
{"kind": "acme_media", "purpose": "external_job", "env_var": "ACME_MEDIA_KEY"}
Run the command it returns, exactly as given. Keep the drop_id.
- The definition:
{
"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
}
}
]
}
secondsisfixedat 5 andcost_affecting: nobody can ask for a longer (costlier) video.{{files.image}}becomes the card's still asdata:image/png;base64,...when the call is sent.- The key goes only to
api.acme-media.test; the video is downloaded fromfiles.acme-media.testwithout it.
manage_connectionwith actionpreview,definitionset to the JSON above, and:
{"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:
{"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": []}
manage_connectionwith actioncreate,definitionset to the same JSON, and:
{"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.
manage_connectionwith{"action": "check_key", "connection_id": "<id>"}, then{"action": "key_check_status", "connection_id": "<id>", "key_check_id": "<key_check_id>"}untilstateisdone(status_code200).- Attach it to the board "Video posts" for the maker in "Creating":
{"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.
- A column step on "Make video" that moves the card to "Creating" when the video is saved:
{"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"}}
- After the limits are set, a card moved into "Make video" (
move_card) makes one call.manage_connection_requestactionlistwith the card's id shows it going fromqueuedtorunningtodone, and the card moves to "Creating" with the video on it. Its receipt names theprovider_job_idand the output file, such asimage_to_video-01a0eb-1.mp4.manage_board_connectionactionusagethen showscounted_micros: 100000andmoney_left_micros: 900000.