Beta sign in

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.

  1. Morning digest A note arrives: which of your tools shipped a security fix overnight.
  2. Price alert If BTC is under $90k, you get a short alert. Either way, the run leaves a receipt.
  3. 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.

  1. 1
    inputrepos["tokio-rs/tokio", "bytecodealliance/wasmtime"]
  2. 2
    fetch · fetch_httpOne Atom request per repohttps://github.com/tokio-rs/tokio/releases.atom
  3. 3
    entries · parse_rssFeed entriestitle, link, published
  4. 4
    with_repo · regex_extractRepo and tag from the linkbytecodealliance/wasmtime · v49.0.3
  5. 5
    unique · dedupeOne row per release idkey: [id]
  6. 6
    new · diff_since_lastOnly releases this pipe has not seen_change: added
  7. 7
    ordered · sortNewest published firstpublished desc, then repo
  8. 8
    format · formatOne text documentNew releases:\n- bytecodealliance/wasmtime v49.0.3
  9. 9
    outputtext and countcount: 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.

pipe:github-releases-digest@^0.1releasestext, count
pipe:security-advisories@^0.1advisoriestext, count
mergerowskind + text from each pipe
formatdigestRelease radar, then each pipe’s text

The 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

Open release-radar · Browse the library · API and MCP