> ## 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.

# Daily page digest to Slack

A routine on a daily cron scrapes a page, writes a three-bullet digest with b3os/qwen3.5-9b and posts it to Slack.

One routine, four products: a `cron` trigger starts it every day at 13:00 UTC, `vision/scrape@1` reads the page,
`infer/chat@1` writes the digest with `b3os/qwen3.5-9b`, and `io.b3os/slack/send-message@1` posts it with the
project's Slack connection from the vault. The recipe publishes the routine, runs it once instead of waiting for
13:00, reads the trigger's next fire time, and deletes the routine. Owner: routines.

Not verified yet: no docs-examples run has covered the runnable snippets on this page.

## The routine

Save it as `routine.json`:

```json
{
  "name": "recipe-cron-scrape-slack",
  "inputs": {
    "type": "object",
    "properties": {
      "url": { "type": "string", "default": "https://news.ycombinator.com" },
      "channel": { "type": "string", "default": "b3os-recipes" }
    }
  },
  "triggers": [{ "type": "cron", "id": "daily", "cron": "0 0 13 * * *", "tz": "UTC", "catch_up": "skip" }],
  "steps": [
    { "id": "page", "uses": "vision/scrape@1", "with": { "url": "${{ inputs.url }}", "formats": ["markdown"] } },
    {
      "id": "digest",
      "uses": "infer/chat@1",
      "with": {
        "model": "b3os/qwen3.5-9b",
        "max_tokens": 300,
        "messages": [
          { "role": "system", "content": "You write a three-bullet digest of a web page for a Slack channel. Plain text, one line per bullet, each starting with '- '. No preamble." },
          { "role": "user", "content": "Page: ${{ inputs.url }}\n\n${{ steps.page.outputs.markdown }}" }
        ]
      }
    },
    {
      "id": "post",
      "uses": "io.b3os/slack/send-message@1",
      "with": {
        "channel": "${{ inputs.channel }}",
        "text": "Digest of ${{ inputs.url }}\n${{ steps.digest.outputs.choices[0].message.content }}"
      }
    }
  ],
  "output": {
    "summary": "Posted a digest of ${{ inputs.url }} to ${{ inputs.channel }}",
    "digest": "${{ steps.digest.outputs.choices[0].message.content }}"
  }
}
```

## Run it

Publish it, then start a run now: the same run the cron starts at 13:00.

**curl**

```bash
curl -s -X PUT https://beta-api.b3os.org/v1/routines/recipe-cron-scrape-slack \
  -H "Authorization: Bearer $B3OS_API_KEY" \
  -H "Content-Type: application/json" \
  -d @routine.json
curl -s "https://beta-api.b3os.org/v1/routines/recipe-cron-scrape-slack/runs?wait=60" \
  -H "Authorization: Bearer $B3OS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```
**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, {}).wait();
console.log(run.status, run.summary);
```
**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"], {}, wait=True)
print(run["status"], run.get("summary"))
```
**CLI**

```bash
b3os routine deploy routine.json
b3os routine run recipe-cron-scrape-slack
```

## What it produced

**Command**

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

```json
{
  "mode": "fixture",
  "run_id": "run_01m6ate50rk8d7ycgx8s9aabkc",
  "run_status": "succeeded",
  "next_fire_at": "2026-11-01T13:00:00.000Z",
  "steps": [
    {
      "id": "page",
      "uses": "vision/scrape@1",
      "status": "succeeded",
      "duration_ms": 0.398
    },
    {
      "id": "digest",
      "uses": "infer/chat@1",
      "status": "succeeded",
      "duration_ms": 521.867
    },
    {
      "id": "post",
      "uses": "core/http@1",
      "status": "succeeded",
      "duration_ms": 74.095
    }
  ],
  "total_ms": 596.812,
  "markdown_chars": 17109,
  "digest_bullets": 3,
  "slack_ok": true
}
```

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

Captured 2026-11-01 from commit an unknown commit, label `fixture` (recorded against a pinned fixture). Case `recipes/cron-scrape-slack`.

## How it works

The `post` step needs a Slack connection in your project's vault: a bot token with `chat:write`, the bot invited to
the channel. The step uses the project's newest Slack connection, or the one you bind in the step's `connections`.
Without the recipes' Slack workspace, the harness runs the same routine with the post step calling Slack's
`api.test` method through `core/http@1`, which echoes the message and needs no token (label `fixture`).

The run's `summary` is the outcome line the run list and the dashboard show. If the post fails, [replay the
run](/routines/runs#replay): the scrape and the digest are copied, and only the post runs again. Delete the routine
(`DELETE /v1/routines/recipe-cron-scrape-slack`) to stop the daily runs.

Owner: Routines. Products: Routines, Vision, Infer, 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

**Command**

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

```json
{
  "mode": "fixture",
  "run_id": "run_01m6ate50rk8d7ycgx8s9aabkc",
  "run_status": "succeeded",
  "next_fire_at": "2026-11-01T13:00:00.000Z",
  "steps": [
    {
      "id": "page",
      "uses": "vision/scrape@1",
      "status": "succeeded",
      "duration_ms": 0.398
    },
    {
      "id": "digest",
      "uses": "infer/chat@1",
      "status": "succeeded",
      "duration_ms": 521.867
    },
    {
      "id": "post",
      "uses": "core/http@1",
      "status": "succeeded",
      "duration_ms": 74.095
    }
  ],
  "total_ms": 596.812,
  "markdown_chars": 17109,
  "digest_bullets": 3,
  "slack_ok": true
}
```

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

Captured 2026-11-01 from commit an unknown commit, label `fixture` (recorded against a pinned fixture). Case `recipes/cron-scrape-slack`.