# Recipe: make a fal video on request

This recipe connects [fal](https://fal.ai) so a board can turn a card's still image into a 5-second video with Kling 2.5 Turbo Pro (image to video) while it works. It is [Let your board use a service while it works](/docs/recipes/connections) with the fal definition ready to copy: read that page for how connections, limits, requests and receipts work.

- Your fal account pays. fal's list price when this was written: $0.35 for a 5-second video, $0.07 for every extra second. The definition fixes the length at 5 seconds, so no request can ask for a longer one. BakedBrie never bills or marks up fal usage.
- The fal key goes only to `https://queue.fal.run` in the `Authorization` header as `Key <key>`. The video is downloaded from fal's file host without it.
- Nothing leaves BakedBrie as a post until a person approves the finished work.

## Before you run it

1. Connect [Claude Code](/docs/connect-claude-code) or [Codex](/docs/connect-codex). `whoami` must show `connections` and `secrets` on.
2. Put your fal key in your own shell: `export FAL_KEY=...`. Never paste it into the chat.
3. Know the board, the column where the maker works (for example "Creating") and the maker agent. The card needs a still image (PNG, JPEG or WebP, up to 8 MB) among its files.

## The prompt

```text
In BakedBrie, let the board "{{BOARD_NAME}}" make fal videos. Use only the BakedBrie MCP tools. Start with whoami and stop if connections is off. First read /docs/recipes/connections-fal-video with read_docs.

1. Find the board, its columns and its maker agent with list_boards and read_board.
2. Call prepare_secret with kind fal, purpose external_job and env_var FAL_KEY. Run the command it returns in my shell exactly as given, then use the drop_id. Never print the key.
3. Call manage_connection with action preview, the fal definition from the recipe, operation_key kling_i2v, inputs {"prompt": "Slow push in"} and sample_files {"image": {"media_type": "image/png", "bytes": 1800000}}. Show me the requests. Nothing is sent.
4. When I agree, create it: manage_connection action create, name "fal", the same definition, credential {"drop_id": "<the drop id>"}, and a new request_id.
5. Attach it: manage_board_connection action attach, handle fal-video, operations ["kling_i2v"], agent_ids [the maker], stage_ids [the maker's column], when_to_use "Only when the brief asks for a video."
6. Add to the maker's instructions (manage_agent, then publish_agent): "If the brief asks for a video, ask fal-video for one from the card's still. When the file comes back, attach it and write the caption block. A post holds one video or images, never both."
7. 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. Suggest $0.35 as the most one call can cost.
```

## The fal definition

Copy it into `manage_connection` `preview` and `create` as `definition` (or paste it into **Edit as JSON** in the web app):

```json
{
  "base_url": "https://queue.fal.run",
  "auth": {
    "header": "authorization",
    "prefix": "Key "
  },
  "fixed_headers": [],
  "key_check": null,
  "output_hosts": [
    "fal.media",
    "*.fal.media"
  ],
  "operations": [
    {
      "key": "kling_i2v",
      "label": "Kling video from an image",
      "description": "Makes a short video from one still image and a motion prompt.",
      "cost_hint": "fal list price 2026-09-28: $0.35 for 5 seconds, $0.07 per extra second",
      "kind": "async_job",
      "inputs": [
        {
          "type": "text",
          "key": "prompt",
          "label": "What should move, and how",
          "max_length": 2500,
          "required": true
        },
        {
          "type": "enum",
          "key": "duration",
          "label": "Length in seconds",
          "values": [
            "5",
            "10"
          ],
          "fixed": "5",
          "cost_affecting": true
        },
        {
          "type": "text",
          "key": "negative_prompt",
          "label": "What to avoid",
          "max_length": 2500,
          "required": false,
          "default": "blur, distort, and low quality"
        }
      ],
      "files": [
        {
          "role": "image",
          "label": "Still image",
          "media_types": [
            "image/png",
            "image/jpeg",
            "image/webp"
          ],
          "max_bytes": 8388608
        }
      ],
      "submit": {
        "method": "POST",
        "path": "/fal-ai/kling-video/v2.5-turbo/pro/image-to-video",
        "body": {
          "prompt": "{{inputs.prompt}}",
          "image_url": "{{files.image}}",
          "duration": "{{inputs.duration}}",
          "negative_prompt": "{{inputs.negative_prompt}}"
        }
      },
      "job_id": "$.request_id",
      "returned_urls": {
        "status": "$.status_url",
        "result": "$.response_url",
        "cancel": "$.cancel_url"
      },
      "status": {
        "path": "/fal-ai/kling-video/requests/{{job.id}}/status"
      },
      "result": {
        "path": "/fal-ai/kling-video/requests/{{job.id}}"
      },
      "status_values": {
        "field": "$.status",
        "pending": [
          "IN_QUEUE",
          "IN_PROGRESS"
        ],
        "succeeded": [
          "COMPLETED"
        ],
        "error_field": "$.error"
      },
      "outputs": [
        {
          "path": "$.video.url",
          "media_types": [
            "video/mp4"
          ],
          "max_bytes": 45000000
        }
      ],
      "cancel": {
        "method": "PUT",
        "path": "/fal-ai/kling-video/requests/{{job.id}}/cancel"
      },
      "poll": {
        "first_after_s": 30,
        "every_s": 15,
        "max_wait_s": 1800
      },
      "output_ttl_s": 3300
    }
  ]
}
```

What each part does:

- `submit` posts to fal's queue for the model `fal-ai/kling-video/v2.5-turbo/pro/image-to-video`. `image_url` gets the card's still as a data URI, which fal accepts in place of a link.
- `duration` is `fixed` at `"5"` and marked `cost_affecting`: nobody can ask for a 10-second video. `negative_prompt` has fal's default.
- fal answers `request_id` with `status_url`, `response_url` and `cancel_url`. fal serves status and results under the first two parts of the model id (`fal-ai/kling-video`), so the definition uses the links fal returns, and the `status`, `result` and `cancel` paths are the fallback.
- The status is `IN_QUEUE`, `IN_PROGRESS` or `COMPLETED`. fal reports a failed job as `COMPLETED` with an `error`, so `error_field` is `$.error`.
- The video link is `video.url`. BakedBrie checks it is an MP4 up to 45 MB before saving it.
- `output_hosts` allows `fal.media` and any `*.fal.media` host, where fal serves results (for example `v3.fal.media`). The example output on fal's model page is a sample file on `storage.googleapis.com`. If a real result ever comes from a host that is not listed, the call ends with `CONNECTION_OUTPUT_HOST_NOT_ALLOWED` after fal has charged for it: add that exact host to `output_hosts` (a wildcard over `googleapis.com` is refused).
- `poll` checks first after 30 seconds, then every 15 seconds, and gives up after 30 minutes (`CONNECTION_TIMED_OUT`, with Check again once).
- There is no `key_check`: fal has no harmless GET for a key. The first real call proves it.

## Attach it and set the limits

```json
{"action": "attach", "board_id": "<board id>", "connection_id": "<fal connection id>", "handle": "fal-video", "operations": ["kling_i2v"], "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_link`. Only a person in the web app can set the limits. Suggested values:

- most one call can cost for `kling_i2v`: **$0.35** (the 5-second price; the length is fixed, so it cannot cost more);
- calls per card: 2 (the first video and one redo after Request changes);
- calls per month: 20; money per month: $7.00 (20 calls at $0.35);
- the connection's monthly ceiling (`ceiling_link`): at least the sum of the boards that use it.

BakedBrie counts each call at $0.35. fal may charge less; check your fal dashboard for the real amount.

## The maker's instructions

Add this to the maker's instructions:

```text
If the brief asks for a video, ask fal-video for one from the card's still. When the file comes back, attach it and write the caption block. A post holds one video or images, never both.
```

The maker then ends its reply with a request block like this, and its run ends:

````
```bakedbrie-connection-request
{"connection":"fal-video","operation":"kling_i2v","inputs":{"prompt":"Slow push in, the child waves, soft morning light"},"files":{"image":"still.png"},"why":"The brief asks for a 5-second Reels video of this still."}
```
````

When the video is saved, BakedBrie starts the maker again with the file's name, and the maker finishes with its caption and:

````
```bakedbrie-attach
{"files":["kling_i2v-01a0f3-1.mp4"]}
```
````

A social post takes one video or images, not both (WoopSocial refuses a mix with `MEDIA_MIXED`), so the maker attaches the video alone.

Instead of the maker asking, a column step can make the video when a card enters a "Make video" column and then move it to "Creating" (`manage_connection_step`, see [the main recipe](/docs/recipes/connections)).

## What the card shows

1. "Waiting on fal. Video requested by Maker. Counted: up to $0.35."
2. The Connection calls panel moves from Sending to Waiting to Done, with the MP4 and its receipt (fal's request id as `provider_job_id`, the file's name, size and hash, and the amount counted).
3. The maker's finished result with the video goes to the checker, then to review. After a person approves, it is delivered like any other finished work.

If something goes wrong, the card says so in plain words:

- fal refused the call (bad input, key rejected): nothing counted; the maker continues without it.
- fal says the account is locked or out of balance: BakedBrie tries a few times over about 2 minutes, then stops. Add funds at fal, then try again.
- BakedBrie cannot tell whether fal accepted the call: "fal may have accepted this request. Check your fal account before sending it again." It stays counted and is never sent again by BakedBrie. Send again (it may charge twice) is only in the web app.
- Not finished after 30 minutes: Check again once, or give up.

Costs are always your own fal account's.
