How a pipe works
You ask your bot to watch things. A pipe is the written-down way it watches, so the same question follows the same steps every time.
Why your bot is better with pipes
Picture one request: tell me every morning if any of my tools shipped a security fix. Without a pipe, the bot starts over each time. With a pipe, that morning check is a fixed routine.
Without pipes
A fresh guess every morning
- It invents a new plan each time you ask.
- Monday’s answer can disagree with Tuesday’s.
- It writes a long new reply even when nothing changed.
- It sometimes names a source it never opened.
- You cannot show what it actually checked.
- It stops when you close the chat.
With pipes
The same routine, every morning
- The same question follows the same steps.
- You get the same kind of answer when the inputs match.
- The reply is the pipe’s result, which is smaller than the raw sources.
- It only reports what the pipe fetched.
- A receipt lists exactly what it checked.
- It runs while you sleep, and the dashboard shows the run.
Example. Another everyday ask: “Alert me if BTC drops below $90k.”
What your bot saves
Measured Oct 18, 2026, on three checks: SEC 8-K filings, a 4-feed news digest, and a 7-symbol price snapshot. Method, table, and caveats.
92% fewer tokens
Across the three checks
9,137 tokens for the pipe results. 116,827 to read the raw sources. Counted with tiktoken o200k_base, 10 runs each.
99.4% fewer
When nothing is new
A quiet SEC check came back in 142 tokens, not 25,574. Get pinged only when something is new: no polling.
30/30
Exact, and replayable
30 of 30 runs matched an independent parse. 29 of 29 replays reproduced the receipt.
Reliable
Same question, same answer. The steps do not change underneath you.
Cheaper
No re-thinking the same task. A repeated step is reused from cache.
Always on
Schedules run without you. The morning check still happens after you close the laptop.
Provable
Every run has a receipt. You can see what was checked, and replay that run later.
Safe
Each pipe declares exactly what it can touch. It does not wander off to other sites.
Visible
See what your agents did. The dashboard lists each run, the result, and the receipt.
A day with your bot
Example. These times illustrate a schedule. They are not a live run.
- Morning digest A note arrives: which of your tools shipped a security fix overnight.
- Price alert If BTC is under $90k, you get a short alert. Either way, the run leaves a receipt.
- You look The dashboard shows both runs, what each one checked, and the receipt.
Anatomy of a pipe
github-releases-digest watches GitHub release feeds. The diagram is the real step order. The values are example data from the recorded snapshot.
- 1inputrepos
["tokio-rs/tokio", "bytecodealliance/wasmtime"] - 2fetch · fetch_httpOne Atom request per repo
https://github.com/tokio-rs/tokio/releases.atom - 3entries · parse_rssFeed entries
title, link, published - 4with_repo · regex_extractRepo and tag from the link
bytecodealliance/wasmtime · v49.0.3 - 5unique · dedupeOne row per release id
key: [id] - 6new · diff_since_lastOnly releases this pipe has not seen
_change: added - 7ordered · sortNewest published first
published desc, then repo - 8format · formatOne text document
New releases:\n- bytecodealliance/wasmtime v49.0.3 - 9outputtext and count
count: 6 in the recorded example
examples/github-releases.pipe.yaml
steps:
- id: fetch
block: fetch_http
for_each: "${inputs.repos}"
with:
url: "https://github.com/${item}/releases.atom"
- id: entries
block: parse_rss
in: "${steps.fetch}"
- id: with_repo
block: regex_extract
in: "${steps.entries}"
- id: unique
block: dedupe
in: "${steps.with_repo}"
- id: new
block: diff_since_last
in: "${steps.unique}"
- id: ordered
block: sort
in: "${steps.new}"
- id: format
block: format
in: "${steps.ordered}"
output:
text: "${steps.format.text}"
count: "${steps.format.count}"
Example output
New releases:
- bytecodealliance/wasmtime dev (2026-10-16T21:32:02Z)
- bytecodealliance/wasmtime v49.0.3 (2026-10-14T17:17:37Z)
- tokio-rs/tokio tokio-1.53.3 (2026-10-13T20:49:48Z)Example data, shortened from the committed snapshot. The full list is longer.
Run receipt
b3:32fcd8189b88e670634706ae5cc0ca8bb11e711f2eb0637dd614bbbabd621e29
That hash covers the pipe content id, the input hash, each step output hash, each recording hash, and the final output hash. pipes replay reruns the snapshot with no network and checks the same receipt. It does not include the run id or the wall clock.
Chaining pipes
release-radar does not fetch those feeds itself. It runs two published pipes, then merges their text. A sub-pipe keeps its own permissions. The parent must allow every host the children use.
text, counttext, countkind + text from each pipeRelease radar, then each pipe’s textThe step that calls a pipe
permissions:
network: [github.com, rustsec.org]
steps:
- id: releases
block: pipe:github-releases-digest@^0.1
with:
repos: "${inputs.repos}"
- id: advisories
block: pipe:security-advisories@^0.1
with:
keyword: "${inputs.keyword}"
- id: rows
block: merge
with:
lists:
- - kind: releases
text: "${steps.releases.text}"
- - kind: advisories
text: "${steps.advisories.text}"
Lockfile pins the range to a content id
[[pipes]]
name = "release-radar"
[pipes.pipes]
"github-releases-digest@^0.1" = "b3:461f0752b1e60498a29505f7cc490a067c99706ec05790b5dbf68f2b8b6a2361"
"security-advisories@^0.1" = "b3:a99d8390695e47234b980e82b71b35bd3754b8e081536837372a051e7b1df661"
^0.1 means 0.1.x. These library pipes are 0.1.0, so the range is ^0.1. A 1.x pipe uses the same syntax with @^1. The pin is the content id, not the version label. A newer match makes the lock stale until you pass --update-lock.
Combined receipt
One hash covers the parent step and every nested step. Example data from one local run of release-radar. The feeds change, so a later run has a different receipt. Replay of that snapshot checks this same hash.
steps:
- id: releases block_version: b3:461f0752b1e60498a29505f7cc490a067c99706ec05790b5dbf68f2b8b6a2361
- id: releases/fetch
- id: releases/new
- id: advisories block_version: b3:a99d8390695e47234b980e82b71b35bd3754b8e081536837372a051e7b1df661
- id: advisories/fetch
- id: rows
- id: digest
receipt: b3:60e9350ce3376611a3b8d71eb74a8fbb9a458f657a15b7cc51f72cb83c514c41
HTTP responses and state reads from both pipes are on the one tape. Replay checks every one of those hashes.
Nesting stops at 8 pipes. A cycle, or a child host the parent did not allow, fails validation.
How your agent calls it
MCP tool call
{
"name": "pipe__release-radar",
"arguments": {
"repos": ["tokio-rs/tokio"],
"keyword": "wasmtime"
}
}
The tool result includes the output and the receipt
{
"status": "succeeded",
"output": { "text": "Release radar\n\n…", "count": 2 },
"receipt": "b3:…"
}
The output text above is example data. Sign in at /login with an invite code, create a key, and call https://asipipes.com/mcp.
Hosted MCP
POST https://asipipes.com/mcp
Authorization: Bearer pk_live_…The tool name is pipe__release-radar. Client config has the JSON for Cursor and Claude.
Get pinged when something new happens
A webhook fires when a run's diff_since_last step finds new records. An empty run stays quiet. The POST body is JSON, so a Grok Bot routine can use the URL as its trigger.
Add a webhook
curl -s -X POST https://asipipes.com/v1/notifications \
-H "Authorization: Bearer $PIPES_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/hooks/asipipes","pipe":"github-releases-digest"}'
MCP tool call
{
"name": "add_notification",
"arguments": {
"url": "https://example.com/hooks/asipipes",
"pipe": "github-releases-digest"
}
}
Check Asipipes-Signature
import hashlib, hmac
def signed(secret, timestamp, body):
mac = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256)
return "sha256=" + mac.hexdigest()
ok = hmac.compare_digest(signed(secret, timestamp, raw_body), header)
The signature covers the timestamp, a dot, and the raw body. Asipipes-Delivery stays the same when a delivery is retried. Leave pipe out to watch the whole workspace. Beta Free includes 100 deliveries a day.
Determinism in 3 bullets
- Same pipe content, same inputs, and the same lock produce the same output. The receipt is how you check.
- An LLM step is cached by a hash of the model, the prompt, and the item. The next run reuses that answer.
- --frozen turns a cache miss into an error, which is what you want in CI.