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.
bashcurl -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.jsoncurl -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 '{}'
tsimport { readFileSync } from "node:fs";import { B3OS } from "@b3os/sdk";const b3os = new B3OS(); // reads B3OS_API_KEYconst 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);
pythonimport jsonfrom b3os import B3OSb3os = B3OS() # reads B3OS_API_KEYdefinition = 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"))
bashb3os routine deploy routine.jsonb3os routine run recipe-cron-scrape-slack
What it produced
bash$ node run.mjs# exit 0
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: 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
bash$ node run.mjs# exit 0
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
.