> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beta.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Approve from Slack with buttons

A routine posts a screenshot to Slack with Approve and Reject buttons and continues when a person presses one, with no API key and no Slack request URL.

A person in Slack decides whether a run goes on. The routine takes a screenshot with `render/screenshot@1`, posts it
with `io.b3os/slack/send-block-kit-message@1` as a message with Approve and Reject buttons, and waits on a
`token: true` step. Each button opens the run's signed wait link, `run.wait_urls.approve`: a page that shows the
screenshot and asks the person to confirm. Their answer completes the wait, and the run replies in the message's
thread. The recipe publishes the routine, starts a run, presses Approve the way a person does, and deletes the routine.

Verified 2026-11-03: this recipe's last run passed against `https://beta-api.b3os.org`, label `fixture` (recorded against a pinned fixture), from commit `6f4f6b98`. The recipe harness runs it again, and a failed run takes this page out of the cookbook.

## The routine

Save it as `routine.json`:

```json
{
  "name": "recipe-slack-approval-buttons",
  "inputs": {
    "type": "object",
    "properties": {
      "url": { "type": "string", "default": "https://beta.b3os.org/routines" },
      "channel": { "type": "string", "default": "b3os-recipes" }
    }
  },
  "steps": [
    { "id": "shot", "uses": "render/screenshot@1", "with": { "url": "${{ inputs.url }}" } },
    {
      "id": "ask",
      "uses": "io.b3os/slack/send-block-kit-message@1",
      "with": {
        "channel": "${{ inputs.channel }}",
        "text": "Approve the screenshot of ${{ inputs.url }}? Open the message to answer.",
        "blocks": "${{ toJSON([{'type': 'header', 'text': {'type': 'plain_text', 'text': 'Approve this screenshot?'}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': format('<{0}|{0}>, ready to post. Your answer goes in this thread.', inputs.url)}}, {'type': 'image', 'image_url': steps.shot.outputs.artifacts[0].url, 'alt_text': format('Screenshot of {0}', inputs.url)}, {'type': 'actions', 'elements': [{'type': 'button', 'style': 'primary', 'text': {'type': 'plain_text', 'text': 'Approve'}, 'url': run.wait_urls.approve + '?decision=approve'}, {'type': 'button', 'style': 'danger', 'text': {'type': 'plain_text', 'text': 'Reject'}, 'url': run.wait_urls.approve + '?decision=reject'}]}, {'type': 'context', 'elements': [{'type': 'mrkdwn', 'text': 'Each button opens a page that asks you to confirm. The link works once, for an hour.'}]}]) }}"
      }
    },
    {
      "id": "approve",
      "name": "Approve the screenshot",
      "wait": {
        "token": true,
        "ms": 3600000,
        "prompt": {
          "title": "Post this screenshot?",
          "body": "A screenshot of ${{ inputs.url }}, taken by this run. Your answer goes in the Slack thread.",
          "image": "${{ steps.shot.outputs.artifacts[0] }}",
          "link": "${{ inputs.url }}"
        }
      }
    },
    {
      "id": "reply",
      "if": "${{ !steps.approve.outputs.timed_out }}",
      "uses": "io.b3os/slack/send-message@1",
      "with": {
        "channel": "${{ inputs.channel }}",
        "thread_ts": "${{ steps.ask.outputs.data.ret.ts }}",
        "text": "${{ steps.approve.outputs.payload.approved == true ? 'Approved through the link.' : 'Rejected through the link.' }}"
      }
    }
  ],
  "output": {
    "summary": "${{ steps.approve.outputs.timed_out ? 'Nobody answered within the hour' : format('{0} through the Slack buttons', steps.approve.outputs.payload.approved == true ? 'Approved' : 'Rejected') }}",
    "decision": "${{ steps.approve.outputs.timed_out ? 'expired' : steps.approve.outputs.payload.decision }}",
    "via": "${{ steps.approve.outputs.timed_out ? 'none' : steps.approve.outputs.payload.via }}"
  }
}
```

`blocks` is one `toJSON` expression, so the screenshot's URL and the link are escaped for Slack. Slack gets five
blocks: a header, a line with the page's link, the screenshot, the two buttons (each `url` is the wait link with
`?decision=approve` or `?decision=reject`) and a line saying the link works once.

## Run it

Publish it and start a run with your channel. The run waits until someone presses a button in Slack.

**curl**

```bash
curl -s -X PUT https://beta-api.b3os.org/v1/routines/recipe-slack-approval-buttons \
  -H "Authorization: Bearer $B3OS_API_KEY" -H "Content-Type: application/json" -d @routine.json
curl -s https://beta-api.b3os.org/v1/routines/recipe-slack-approval-buttons/runs \
  -H "Authorization: Bearer $B3OS_API_KEY" -H "Content-Type: application/json" \
  -d '{"input": {"channel": "C0123LAUNCH"}}'
```
**TypeScript**

```ts
import { readFileSync } from "node:fs";
import { B3OS } from "@b3os/sdk";

const b3os = new B3OS(); // reads B3OS_API_KEY
const def = JSON.parse(readFileSync("routine.json", "utf8"));
await b3os.routines.publishRoutine(def.name, def);
const run = await b3os.routines.createRun(def.name, { input: { channel: "C0123LAUNCH" } });
console.log(run.id, run.status);
```
**Python**

```python
import json
from b3os import B3OS

b3os = B3OS()  # reads B3OS_API_KEY
definition = json.load(open("routine.json"))
b3os.routines.publish_routine(definition["name"], definition)
run = b3os.routines.create_run(definition["name"], {"input": {"channel": "C0123LAUNCH"}})
print(run["id"], run["status"])
```
**CLI**

```bash
b3os routine deploy routine.json
b3os routine run recipe-slack-approval-buttons --no-wait
```

## What it produced

**Outputs**

```json
{
  "mode": "fixture",
  "run_id": "run_01m6h7a3e5cn6kshvh8vr62cnf",
  "run_status": "succeeded",
  "steps": [
    {
      "id": "shot",
      "uses": "render/screenshot@1",
      "status": "succeeded",
      "duration_ms": 666.612
    },
    {
      "id": "ask",
      "uses": "core/http@1",
      "status": "succeeded",
      "duration_ms": 93.82
    },
    {
      "id": "approve",
      "uses": "core/wait@1",
      "status": "succeeded",
      "duration_ms": 895.616
    },
    {
      "id": "reply",
      "uses": "core/http@1",
      "status": "succeeded",
      "duration_ms": 77.416
    }
  ],
  "message_blocks": [
    "header",
    "section",
    "image",
    "actions",
    "context"
  ],
  "buttons": [
    {
      "text": "Approve",
      "style": "primary",
      "opens_wait_link": true
    },
    {
      "text": "Reject",
      "style": "danger",
      "opens_wait_link": true
    }
  ],
  "page_status": 200,
  "page_shows_title": true,
  "page_shows_image": true,
  "page_preselects_approve": true,
  "answer_status": 200,
  "second_answer_status": 410,
  "decision": "approve",
  "via": "link",
  "answer_to_finish_ms": 110
}
```
**Command**

```bash
$ node run.mjs
# exit 0
```

The outputs this recipe publishes (the `publish.outputs` list in its recipe.json), with workspace identifiers redacted. The run's stdout, stderr and other outputs are not published.

Captured 2026-11-03 from commit `6f4f6b98`, label `fixture` (recorded against a pinned fixture). Case `recipes/slack-approval-buttons`.

## How it works

The buttons carry a `url` and nothing else, so Slack only opens the link: the app needs no request URL and no signing
secret, and nothing of yours has to be reachable from Slack. If the app has Interactivity turned on, Slack also sends
it a payload for each press, which it has to acknowledge; with Interactivity off it sends none.

Opening the link answers nothing. `?decision=approve` preselects Approve and the person confirms on the page, so
link previews and scanners that fetch the URL decide nothing. The page shows the prompt and the screenshot, signed
fresh each time it is served. The link works once: a second press gets the answered page, and a program gets
[`wait_already_answered`](/errors#wait_already_answered). It stops working when the step's `ms` deadline passes (an
hour here), when the run ends, or when someone answers through the API instead, with the signal `approve`. See [Ask a
person](/routines/definitions#ask-a-person).

The answer arrives as `steps.approve.outputs.payload`, `{decision, approved, comment, via: "link"}`, and the `reply`
step posts it in the thread. The `text` field is what notifications show; it leaves the link out, so a lock screen
never shows it. Slack's answer repeats the blocks, so the link is in the `ask` step's outputs: a key without
`routines:run` sees `[wait link]` there.

The two Slack steps need a Slack connection in your project's vault: a bot token with `chat:write`, the bot invited to
the channel. Without the recipes' Slack workspace, the harness runs the same routine with both Slack steps calling
Slack's `api.test` method through `core/http@1`, form-encoded. Slack echoes the fields it got, blocks and buttons
included, and needs no token (label `fixture`). The recipe then reads the link from that echo, as a person reads it
from the message, and the reply has no thread to go to.

Owner: Routines. Products: Routines, Render, Actions and triggers, Vault.

The last green run used fixture origins in place of the slack workspace, so the outputs below come from the fixture.

## Output of the last green run

**Outputs**

```json
{
  "mode": "fixture",
  "run_id": "run_01m6h7a3e5cn6kshvh8vr62cnf",
  "run_status": "succeeded",
  "steps": [
    {
      "id": "shot",
      "uses": "render/screenshot@1",
      "status": "succeeded",
      "duration_ms": 666.612
    },
    {
      "id": "ask",
      "uses": "core/http@1",
      "status": "succeeded",
      "duration_ms": 93.82
    },
    {
      "id": "approve",
      "uses": "core/wait@1",
      "status": "succeeded",
      "duration_ms": 895.616
    },
    {
      "id": "reply",
      "uses": "core/http@1",
      "status": "succeeded",
      "duration_ms": 77.416
    }
  ],
  "message_blocks": [
    "header",
    "section",
    "image",
    "actions",
    "context"
  ],
  "buttons": [
    {
      "text": "Approve",
      "style": "primary",
      "opens_wait_link": true
    },
    {
      "text": "Reject",
      "style": "danger",
      "opens_wait_link": true
    }
  ],
  "page_status": 200,
  "page_shows_title": true,
  "page_shows_image": true,
  "page_preselects_approve": true,
  "answer_status": 200,
  "second_answer_status": 410,
  "decision": "approve",
  "via": "link",
  "answer_to_finish_ms": 110
}
```
**Command**

```bash
$ node run.mjs
# exit 0
```

The outputs this recipe publishes (the `publish.outputs` list in its recipe.json), with workspace identifiers redacted. The run's stdout, stderr and other outputs are not published.

Captured 2026-11-03 from commit `6f4f6b98`, label `fixture` (recorded against a pinned fixture). Case `recipes/slack-approval-buttons`.