Skip to content
CLI reference

CLI reference

Global options

tundish <command>
Option Default Meaning
-h, --help Print help. Each command also accepts these flags.
-V, --version Print the version.

tundish uses one database per user. The database: setting in ~/.config/tundish/config.yml selects its path. The default is ~/.local/state/tundish/tundish.db. If the config file is absent, every default applies. Home comes from the passwd database. HOME and XDG environment variables do not change these paths. The config path is fixed.

Item

Field Type Meaning
id Integer Item id, unique across all queues in the database.
queue String Queue name.
key String or null Optional label set by the publisher, such as slack:C123.
payload JSON Published value. Object key order is kept.
created_at Integer Creation time in Unix seconds.

Filters and templates receive this object. show and list also include an acks array when at least one subscription has acked the item. Each entry has subscription, owner, and at in Unix seconds.

publish

tundish publish <queue> [--key <k>] [<json>]

Publishes one item and prints its id. Without <json>, it reads JSON from stdin. The queue name must be nonempty. The payload can be any JSON value. --key sets an optional label. Reusing a key adds another item; it does not replace or deduplicate items.

The insert and the queue’s retention trim commit in one transaction.

subscribe

tundish subscribe <name> --as <subscriber> [--queue <q>] [<rule flags>]

With --queue, creates or replaces the named subscription and binds the subscriber to it. Omitted rule flags take their defaults, including when replacing a subscription. Without --queue, binds the subscriber to an existing subscription. Rule flags require --queue.

Subscription and subscriber names must contain 1 to 64 characters from A-Z a-z 0-9 . _ : -. An invalid name exits 2. For example, tundish subscribe 'in box' --as reviewer --queue slack prints:

tundish: a subscription name is 1 to 64 of A-Z a-z 0-9 . _ : -, not "in box"

A subscriber is a stable name, such as reviewer. Repeating the binding keeps its position. Replacing rules on the same queue keeps existing positions and claims. A filter change does not revisit items behind the position, even if they now match. This applies to both share modes. Changing from share one to share all clears leases and carries the position to the subscribers. Changing to share one starts from the lowest subscriber position. Moving the subscription to another queue resets its positions and removes its acks and leases.

The command prints up to three lines, in this order:

When Output
--queue creates a subscription created <name>
--queue replaces rules on the same queue replaced the rules of <name>
--queue moves the subscription to another queue moved <name> from <old queue>; its positions, acks, and leases were reset
This call binds the subscriber bound <subscriber> to <name>
Always rules: <the stored rules as compact JSON>

Without --queue, no creation, replacement, or move line prints. An existing binding prints no bound line. Run tundish subscribe <name> --as <subscriber> with no other flags to read the stored rules. If the subscriber is already bound, only rules: prints.

jaq evaluates jq expressions over JSON. Filters and templates see the item object, including .payload. In a filter, false and null are false; all other values are truthy.

Rule flag Default Meaning
--filter <jaq> Match every item Evaluate per item. Any truthy output makes it match.
--select since-last-read|latest since-last-read Select unseen matching items. latest needs --limit and refuses --share one.
--limit N No item limit Maximum items per pull of this subscription. At least 1.
--consume on-read|on-ack on-read Count delivery as done, or require an ack.
--share all|one all Deliver to every subscriber, or share items among subscribers.
--every <duration> No interval Interval for due pulls, per subscriber.
--template <jaq> [queue#id] key payload-json Render each item. The default omits the key when absent.
--lease <duration> 600s Lease duration for share one with on-ack.
--max-attempts N 3 Maximum leases before parking an item. Must be positive.
--start all|new all Start with all retained items, or only future items. Applies to each new subscriber under share all and the initial subscription position under share one.

Durations are positive integers with an optional unit: 90, 30s, 10m, 2h, or 1d. A bare integer means seconds.

since-last-read selects oldest first. latest selects the newest N unseen matching items and prints them oldest first. It moves the position to the newest scanned item and skips older unseen items. Each pull scans at most 1000 items per subscription. For latest, this is the newest 1000 unseen items. If the byte budget stops a latest batch, the pull keeps the newest selected items that fit and skips older ones. It still prints them oldest first and moves the position to the newest scanned item. The status line reports more=0 because the skipped items do not return. See Due pulls for a subscription with no room to deliver an item.

Filter and template expressions compile when the subscription is created or replaced. At pull time, a filter error skips the item and raises errors= in the status line. tundish status keeps the most recent filter error text in last_error.

A template joins multiple outputs with newlines. Strings print as text; other values print as JSON. For on-ack, each item’s output starts with [queue#id] before the template output. For on-read, the template output prints as-is. If a template fails, the default item form prints with (template error: <error>) appended. Template errors do not raise errors=. An empty result uses the default form.

Delivery rules

Share Consume Result after a successful pull
all on-read Each subscriber sees each selected item once.
all on-ack Each subscriber sees each selected item once. The first ack in the subscription wins. unacked= counts earlier unacked items when the pull delivers items.
one on-read Each selected item goes to exactly one subscriber. The pull records its ack.
one on-ack The pull leases each selected item to one subscriber. An expired lease makes it available again.

With share one, the subscription has one forward position for all its subscribers. A pull reads new items after it. The position moves past each item evaluated, whether delivered, filtered out, or skipped after a filter error. A held lease does not block new work for other subscribers.

With share one and on-ack, a live lease hides the item from every subscriber, including its holder. If it expires before an ack, the next pull retries the item ahead of new items, within --limit. A retry does not run the filter again. The item matched when first leased.

When the last allowed lease under --max-attempts expires without an ack, the item is parked. A pull reports it once as parked= in the status line. Parked items are never retried. Raising --max-attempts does not bring them back. They remain available through show and list until retention removes them.

Acks and leases belong to a subscription. Subscriptions on the same queue do not share a claim.

unsubscribe

tundish unsubscribe <name> --as <subscriber>

Removes the subscriber’s binding and position. The subscription remains, even after its last subscriber leaves. Prints unbound <subscriber> from <name>; its read position was removed and exits 0. Under share all, a later subscribe starts a new position under the subscription’s start rule. Under share one, the subscription keeps its position. An absent binding exits 2.

pull

tundish pull <name> --as <subscriber> [--max-bytes N]
tundish pull --due --as <subscriber> [--max-bytes N]

--as is required. A named pull reads one subscription and ignores its --every interval. See Due pulls for --due. The name and --due are mutually exclusive.

--max-bytes defaults to 1024 and must be at least 400. A smaller value exits 2 with tundish: --max-bytes must be at least 400.

A pull prints and flushes the batch before committing delivery. A failure before commit leaves delivery uncommitted. The next pull can repeat printed items. Delivery is at least once.

Earlier unacked items alone do not cause output. The unacked= count is set only when the pull delivers items; otherwise it is 0. The parked=, errors=, and dropped= counts print once, even without items. The pull that reports them resets them. These rules apply to both named and due pulls.

Due pulls

pull --due --as <s> visits the subscriber’s due subscriptions in order of next_at, then name. A subscription is due when next_at is no later than the current time. After a delivery, next_at is the last delivery time plus --every, using zero seconds when no interval is set. A named pull also updates this time when it delivers an item. An empty pull or a pull that prints only a status line does not move next_at.

A due pull uses one transaction and one byte budget across all subscriptions. It commits all delivery changes or none. Each subscription uses the budget left by those before it. A subscription with nothing to report prints nothing and uses no budget.

If a subscription has no room for its status line plus the start of one item, it prints nothing and stays due. It comes first on the next due pull. If its status line fits but its first item does not, and other output came before it, it prints delivered=0 more=1 and keeps its position. This also applies to latest.

The first item of the whole output is always delivered. If needed, it is cut to fit and ends with … tundish show <id>. The cut item counts as delivered. Output never exceeds the budget, including status lines and newlines. These budget rules also apply to a named pull, which reads one subscription.

A bound subscriber with nothing due and nothing to report gets no output and exit 0. A subscriber with no bound subscriptions gets exit 2 and:

tundish: <who> holds no subscription, so nothing can be due: tundish subscribe <name> --as <who>

See the gaff guide for an interval used with a hook.

Byte budget and status line

Each subscription that prints anything starts with one status line, followed by its item lines. The fields appear in this order:

tundish: subscription=<name> subscriber=<subscriber> delivered=<N> more=<0|1> unacked=<N> parked=<N> errors=<N> dropped=<N> lease=<seconds>
Field Meaning
subscription Subscription name.
subscriber Subscriber name from --as.
delivered Items delivered in this batch.
more 1 when the batch stopped before finishing the scan or delivering all matching items. Pull again by the subscription name. A latest batch that delivered items skips older items and reports 0.
unacked Earlier matching items delivered under share all with on-ack that nobody in the subscription acked. Set only alongside delivered items; otherwise 0.
parked Items parked since the last report.
errors Items whose filter failed since the last report.
dropped Unread matching items removed by retention since the last report. Includes items trimmed under an open lease in share one.
lease Seconds the lease holds for a share one, on-ack batch with delivered items; otherwise 0.

Each subscription stops at its item limit, the byte budget, or the scan limit. Later items that do not fit wait for another pull under since-last-read. See Due pulls for budget allocation and truncation.

The ack command is documented by tundish prime and the tundish skill. It takes the item id from [queue#id] and the names from the status line. Inspect unacked items with tundish list <queue> --unacked --subscription <s>. Inspect filter error text with tundish status.

Handler deadline

When GAFF_TIMEOUT_MS contains an unsigned integer, tundish uses it as a deadline in milliseconds. It reserves one quarter of that time, up to 100 ms, for exit and flush. If the deadline passes before commit, the pull rolls back and exits 1. gaff discards the failed handler’s output.

ack

tundish ack <id> --as <subscriber> [--subscription <name>]

--as is required. --subscription is required only when the subscriber holds more than one subscription on the item’s queue. An explicit subscription must be bound to that subscriber and read the item’s queue.

Result Output Exit
First ack in the subscription acked <id> 0
Repeat ack by the owner acked <id> 0
Another subscriber already acked it already acked by <owner> at <unix time> 3
Another subscriber holds a live lease leased by <owner> until <unix time> 3

An ack removes the lease. Exit 2 covers a missing item, an ambiguous subscription, or a subscriber with no subscription on the item’s queue. It also covers an explicit --subscription that the subscriber does not hold or that reads another queue.

show

tundish show <id>

Prints the full item as formatted JSON, with each subscription’s ack. A missing item exits 2.

list

tundish list [<queue>] [--unacked [--subscription <name>]] [--limit N]

Without a queue, prints queue names and item counts separated by a tab, in queue name order. Only queues with retained items appear.

With a queue, prints one JSON item per line. --limit defaults to 50. It selects the newest N items and prints them oldest first. A limit of 0 prints no items. --limit=-1 removes the limit; --limit -1 is a usage error.

--unacked selects items with no ack in any subscription. With --subscription, it selects items with no ack in that subscription. --subscription requires --unacked. These options and --limit only affect output when a queue is named. Listing does not change a position.

status

tundish status

Prints one tab-separated line per subscription and subscriber binding, ordered by subscription then subscriber:

<subscription>	<subscriber>	queue=<q>	cursor=<n>	unread=<n>	next_at=<t>	dropped=<n>	errors=<n>	parked=<n>	last_error=<json string or empty>

For example:

inbox	reviewer	queue=slack	cursor=1	unread=0	next_at=1002	dropped=0	errors=0	parked=0	last_error=

cursor= shows the subscriber’s position under share all and the subscription’s position under share one. With share all, unread= counts items after the position before filtering. With share one, it counts unacked items after the position plus expired leases waiting for a retry. Live leases and parked items do not add to that count.

next_at is the earliest time for a due pull. It starts at 0. dropped= counts unread matching items removed by retention and not yet reported in a pull. For share one, it includes items trimmed under an open lease. errors= counts filter failures not yet reported in a pull. parked= counts parked items, including those already reported. last_error keeps the most recent filter error as a JSON string, or is empty if none has occurred. Reporting errors in a pull resets their count but never resets last_error.

check

tundish check

Validates the config and every subscription’s rules and expressions. Prints ok on success.

prime

tundish prime

Prints a short description and command examples for an agent’s context. The output depends only on the binary version. It does not open the database or load the config.

The ack command in its output is:

tundish ack <id> --as <subscriber> [--subscription <s>]

docs

tundish docs [topic]
tundish docs list
tundish docs search <query>
tundish docs --all

Without a topic, or with list, prints the bundled page listing. A topic is a filename without .md, such as getting-started. search searches the pages. -a or --all prints every page and cannot accompany a topic or query. This command does not open the database or load the config. An unknown topic, missing search query, or search with no matches exits 1.

Package commands

tundish help [command] prints help for the command or the main command listing.

These package commands are hidden from the main help:

Command Meaning
tundish gen-man <dir> Write man pages into a directory.
tundish gen-completions <shell> Print completions for bash, elvish, fish, powershell, or zsh.

Configuration

The config file is ~/.config/tundish/config.yml. Home comes from the passwd database. Publishers, the gaff handler, and the agent use the same database because they read this file as the same user.

format: 1
database: /path/to/tundish.db
queues:
  slack:
    max_items: 10000
    max_age: 7d
Key Default Meaning
format Required in an existing file Must be 1.
database ~/.local/state/tundish/tundish.db The user’s database path.
queues Empty map Per-queue retention settings.
queues.<queue>.max_items Unset for a named queue Count limit, at least 1. Does not remove unread items.
queues.<queue>.max_age Unset Age limit as a duration. Removes old items even if unread.

A queue absent from queues gets max_items: 10000 and no age limit. A named queue gets only the limits specified for it. An entry of {} sets neither limit. Unknown config keys are errors. Paths in the file are literal: ~ is not expanded, and a relative database path is relative to the command’s working directory.

Retention runs when publishing to the queue. max_items can keep more than its limit while subscribers have unread items. Under share all, it protects each subscriber’s position. Unacked items already shown can be removed.

Under share one, the retention floor comes from the subscription’s position and its lowest open lease. An open lease is neither acked nor parked. The count limit keeps items after the position and from the lowest open lease onward. Only subscriptions with a bound subscriber hold retention. Parked items do not hold retention.

max_age removes items older than the configured duration regardless of position or lease. Unread matching items removed by retention contribute to dropped= in the status line. For share one, an item trimmed under an open lease also counts. The count stays pending until a pull reports it once.

Exit codes

Code Meaning
0 Success, including a pull with no output when the subscriber is bound.
1 Error, including invalid JSON, expression errors, I/O or database errors, a missed deadline, or a failed docs request.
2 Usage error, invalid config, names or rule values, unknown subscription, missing binding (including pull --due for a subscriber with no subscriptions), or missing item for show or ack.
3 An ack met another subscriber’s ack or live lease.

For a missing binding, the message names tundish subscribe <name> --as <subscriber>. For an unknown subscription, the message is no subscription named <name>.