Older versions collects the release notes for every Binions build from the first tagged release through the end of the 0.5.0-beta line, in one place. Each entry keeps its lead summary and what changed; the At-a-glance tables and cross-links from the original per-version pages are dropped for brevity. For the current releases and the roadmap, see the main Release Notes.
All additive. Every release in the
0.xline is backward-compatible — existing playbooks and configurations keep working. The beta turns licence enforcement on by default; see the apt upgrade procedure.
The multi-provider broker, the daemons and event bus, and the industrial Modbus integration.
Released 18 May 2026 — Multi-provider broker — more channels, same vocabulary.
Binions v0.1.0 was the first tagged release of the platform. Released on 18 May 2026, its theme is multi-provider broker: more ways to connect, without making your playbooks any more complicated. It added five new communication channels to daemons you already use — and it did so without growing the playbook vocabulary you have to learn.
The HTTP edge can now route WebSocket (ws and wss), Server-Sent Events (SSE), gRPC, and gRPC-Web, in addition to plain HTTP and HTTPS. The same routing you already use for web requests now reaches live, streaming, and RPC-style clients.
A mailbox is no longer limited to IMAP email. It can now also be an MQTT or AMQP broker — so sensor feeds and message queues arrive through the same “a message came in” door as your inbox.
File transfers gained new destinations. Alongside S3 / MinIO, a transfer can now target SFTP, FTP, or the local filesystem — selected on the step, with no change to how the move is described.
The webhook caller can now send SOAP requests over its existing send operation, so playbooks can talk to older enterprise APIs that still expect SOAP.
This release also introduced the platform’s continuous-integration safety net: a dependency-advisory check, an unused-dependency check, and an event-contract snapshot, all run on every change. You see this only as a quality signal — every release ships after passing these gates.
Released 20 May 2026 — Modbus service — the 13th daemon, bringing industrial automation.
Binions 0.2.0-alpha.1 is the second tagged release, and it brought Binions onto the factory floor. Released on 20 May 2026, its theme is the Modbus service — the platform’s thirteenth daemon — which lets your playbooks read from and write to industrial PLCs and I/O modules over MODBUS. The same event-driven automations that handle your email and APIs can now react to, and control, real hardware.
The headline of 0.2.0 is a brand-new daemon dedicated to industrial automation. It speaks two transports: MODBUS TCP/IP (native Modbus over a network) and RTU-over-TCP (classic RTU framing tunnelled over TCP, for example through a serial gateway). With it, Binions can take part in the same automations as your mailboxes, databases, and web hooks — only now the other side of the conversation is a programmable logic controller.
Released 20 May 2026 — Industrial Modbus, completed.
Binions 0.3.0-alpha.1 is the third tagged release, published on 20 May 2026. Where v0.2.0 introduced the Modbus daemon, this release rounds it out — adding the advanced industrial capabilities that take the integration from “reads and writes registers” to “covers the common factory-floor scenarios end-to-end.” With these additions, the platform’s vocabulary of generic operations grows to roughly 45.
Every change in this release lands inside the Modbus service; the other twelve daemons are untouched. Five capabilities arrived.
One more operational improvement ships alongside them:
The edge & interactive layer plus real playbook control flow; the vocabulary held at 45.
Released Rolling, May–June 2026 — The edge & interactive layer, plus a faster engine.
Binions 0.4.0 is the development line that turned Binions from a back-office automation engine into a connected, interactive platform. Where v0.1–v0.3 built the event bus, the daemons, and the industrial integration, the 0.4.0 line opened Binions up to the outside world — inbound web requests, live browser pages, AI agents, more databases — and gave playbooks real control flow. It was a rolling series of additive builds that led directly into the first beta.
Delivery guarantees, a hardened security surface, the downloadable Claude skill, and full MCP-deploy equivalence.
Released 14 June 2026 — First beta — delivery guarantees & security hardening.
v0.5.0-beta.1 is the first beta — the release where Binions stopped growing features and started earning trust. The capabilities built across the 0.4.0 line were settled; this release put the whole platform through a reliability and security gate so it behaves predictably under load, recovers from failure, and protects its interactive surface. It marks the end of the alpha phase.
Released 14 June 2026 — A downloadable Claude skill for writing playbooks.
v0.5.0-beta.2 ships a downloadable Claude skill that teaches an AI assistant to write correct, runnable Binions playbooks. Install it into Claude and the assistant stops guessing the playbook language — it knows the grammar, every generic verb, the events they emit, the common patterns, and how to validate and deploy what it writes. It is a free addition for every customer, and it does not change how the platform itself runs.
Released 15 June 2026 — Full operator-equivalence for MCP deployment.
v0.5.0-beta.3 makes deploying a playbook through the MCP server as powerful as an operator dropping one on the host over SSH. An AI assistant (or any MCP client) that holds the deploy credential can now use the full playbook vocabulary, seed the secrets a playbook needs, target any namespace, and read back exactly how a run finished.
${secret.KEY}), run ssh commands, and call external web endpoints — the same actions an operator has when deploying by hand.put-secret tool writes a secret to the host so ${secret.KEY} resolves in any playbook. The value travels over the authenticated channel and is never written to a log — only a short fingerprint is recorded for audit.provisioning, business, teardown, or the default mcp-deployed area; list-playbooks spans them all; and get-playbook returns a deployed playbook’s YAML and summary.Released 15 June 2026 — MCP deployment write-path fix.
v0.5.0-beta.4 is a small fix that completes the previous release. It lets MCP deployment write to every playbook area and seed secrets on a fresh install, which a tightened file-system sandbox had been blocking.
provisioning, business, or teardown, and writing a secret with put-secret, now work as intended. Found by end-to-end testing on a live instance.Argument validation, parallel steps with retries, sturdier email ingestion, and reliable table indexes.
Released 30 June 2026 — Argument validation catches mistakes before you deploy.
v0.5.0-beta.5 makes playbook validation catch argument mistakes before you deploy. Until now validate checked only the shape of a playbook; a misspelled or missing argument slipped through and failed later, at run time. Now the validator knows every verb’s real arguments and rejects a bad one up front.
binions-cliconsole validate and the MCP deploy-playbook tool now reject an unknown field, a missing required field, or a wrong type in a step’s with: — instead of accepting it and stopping the run later. Interpolation placeholders (${...}) are understood and never trip the type check.Released 30 June 2026 — Parallel steps run; opt-in step retry.
v0.5.0-beta.6 makes parallel steps actually run, and adds opt-in retries to any step. Two workflow-engine improvements: a parallel: step now runs its children at the same time, and any step can be told to retry with a back-off before it gives up.
parallel: block now runs its run: children at the same time and merges their results back into the run; a child given an id: feeds ${steps.<id>}. If any child fails, the others still finish and then the run stops — the same halt-on-error behaviour as a normal sequence.retry: { max_attempts, backoff_ms } to a step and a transient failure is retried with an exponential back-off; if it still fails after the last attempt, the run stops and a dead-letter record is emitted. Pair retries with idempotent steps (an upsert on a unique index) since a retry re-sends the action.# Two independent lookups at once, then a retried write
steps:
- parallel:
- id: crm
run: webhook.send
with: { endpoint: crm-lookup, data: ${trigger} }
- id: geo
run: webhook.send
with: { endpoint: geo-lookup, data: ${trigger} }
- run: database.upsert
retry: { max_attempts: 3, backoff_ms: 500 }
with:
table: contacts
on_conflict: [email]
row: { email: ${trigger.email}, region: ${steps.geo.region} }
Released 30 June 2026 — Operator dead-letter purge + safer inline pages.
v0.5.0-beta.7 adds an operator command to clear stuck failed actions, and makes inline pages safe against injected data. Two hardening improvements to round out the workflow engine.
binions-cliconsole purge-dlq --service <name> reports the parked count and removes them (with a --dry-run to look first). It is an operator maintenance command, deliberately not a playbook verb.show.register_page / show.update_page and drop live data into it, that data is now HTML-escaped by default, so a value coming from an email or a form cannot inject markup into the page. Your own template markup stays intact — structure is trusted, data is escaped.Released 30 June 2026 — Sturdier IMAP email ingestion.
v0.5.0-beta.8 makes email ingestion sturdier. The mailbox listener can watch several folders at once, picks up existing unread mail the first time it connects, and reconnects on its own after a dropped connection.
imap.folders: [INBOX, Invoices] to a mailbox and each folder gets its own live connection and its own progress marker. Leave it out and the single mailbox folder is watched as before.Released 1 July 2026 — Declared indexes are always applied.
v0.5.0-beta.9 makes sure a table always gets the indexes it declares. A fix so that declaring an index on a table that already exists actually creates it — which in turn makes “insert or update on conflict” work reliably.
database.upsert) needs a unique index on the conflict columns. Without it, every such write failed. This fix restores that pattern on tables that were created before their unique index was declared.Server-side aggregation, or-groups and time-window filters, an exact-number fix, and cross-table joins.
Released 1 July 2026 — Aggregates, or-groups, and time filters.
v0.5.0-beta.10 gives playbooks real reporting queries: grouped totals, alternatives, and time windows. A new database.aggregate verb groups and totals rows on the server, and the query filter gains or groups and time-based operators — all still structured YAML, never raw SQL.
database.aggregate — grouped totals. Group by one or more columns and compute sum, avg, count, min, or max, with an optional having filter on the totals. “Revenue per category” and “events per day” are now a single step instead of pulling every row back.or groups in a filter. A where can now express alternatives: match one set of conditions or another. Groups nest, and they work on every filtering verb.within, before, and after operators take a duration (24h, 7d) or an absolute date, so “the last 24 hours” or “older than a week” is expressible directly. The daemon works out “now” on the server.# Revenue per category, paid orders from the last 30 days, biggest first
- run: database.aggregate
with:
table: orders
group_by: [category]
aggregates:
- { fn: sum, col: amount, as: revenue }
- { fn: count, col: "*", as: order_count }
where: { status.eq: paid, created_at.within: 30d }
having: { revenue.gt: 1000 }
order_by: revenue
order_dir: DESC
Released 1 July 2026 — Money columns come back as numbers.
v0.5.0-beta.11 fixes exact-number columns coming back empty. A currency amount or any high-precision number now returns as a number instead of nothing — and grouped sums and averages stop reading as empty.
sum and avg from the previous release reading as empty, since a total over a whole-number column is itself a high-precision number.Released 1 July 2026 — Refreshed Claude skill.
v0.5.0-beta.12 refreshes the downloadable Claude skill so it matches the current platform. The skill’s references were brought back in line with how the daemons actually work — corrected field names, honest notes on a few behaviours, and the newest capabilities documented.
Released 1 July 2026 — Join related tables in a query.
v0.5.0-beta.13 lets a query join related tables. database.query gains an optional join so you can read a row together with the rows it references — an order with its customer, an invoice with its supplier — and alias the columns so nothing collides. Still structured YAML, never raw SQL.
database.query. Add a join to read matching rows from another table in the same query. An inner join keeps only rows that match on both sides; a left join keeps every base row and leaves the joined columns empty where there is no match.table.column to be unambiguous, and give a column an alias ({ col: customers.name, as: customer }) so two same-named columns — two ids, say — stay distinct in the result.# Each paid order together with its customer's name, biggest first
- run: database.query
with:
table: orders
join:
- table: customers
type: left
on: { left: orders.customer_id, right: customers.id }
columns:
- { col: orders.id, as: order_id }
- orders.total
- { col: customers.name, as: customer }
where: { orders.status.eq: paid }
order_by: orders.total
order_dir: DESC
Timestamp writes, broker publish on mail.send, verifiable attachments, watch folders on every backend, and the bucket registry (46 → 48).
Released 2 July 2026 — Timestamp writes that just work.
v0.5.0-beta.14 makes timestamps land as timestamps. A value interpolated into a playbook is a string — and until now, writing an ISO-8601 string into a PostgreSQL timestamp column could be refused by the database. The daemon now checks the target table's real column types on every write and binds timestamps properly, so date-stamped rows just work.
write, upsert, update, delete — the database daemon reads the target table's declared column types and binds an ISO-8601 string as a real timestamp wherever the column is a timestamp column. Previously PostgreSQL refused the assignment (“column is of type timestamp with time zone but expression is of type text”).set: values on an update, and the plain where comparators on update/delete. Read-side time filters (.within .before .after) already handled this.# This row now lands with real timestamps — no casts, no workarounds
- run: database.write
with:
table: invoices
row:
supplier: ${trigger.from}
received_at: ${trigger.received_at} # ISO-8601 string -> timestamptz
Released 2 July 2026 — Broker publish on mail.send.
v0.5.0-beta.15 lets mail.send talk back to message brokers. The mailbox daemon has long listened on MQTT, AMQP, Redis and WebSocket sources; now the same mail.send verb can publish to them. Give it a payload and a target and the frame goes out over the registered mailbox's own protocol — no new verb, no new configuration.
mail.send with a payload publishes a frame: an MQTT topic publish, an AMQP routing-key publish (delivery confirmed), a Redis channel PUBLISH, or a WebSocket text frame to the connected feed.target picks the destination. The MQTT topic, Redis channel or AMQP routing key — required for those three, ignored for WebSocket (the frame goes to the socket the mailbox is connected to). Kafka publishing is available in kafka-enabled builds.Fact.Mail.Sent is stamped with the transport used (smtp or the broker protocol) and the target it went to. Consume-only sources (SSE, IMAP-as-feed) refuse a publish with a clear error.to: and subject: are only required when you actually send e-mail — a broker publish needs no dummy addressing fields.# Acknowledge a processed reading back to the device's broker
- run: mail.send
with:
from_alias: sensors # a registered MQTT mailbox
payload: '{"status":"processed","id":"${trigger.message_id}"}'
target: "devices/ack" # MQTT topic
Released 2 July 2026 — Backlog sweep — attachments, watch folders, intervals, strict arguments.
v0.5.0-beta.16 is a platform-wide sweep: attachments arrive with their bytes, watched folders really watch, schedules take plain intervals, and typos stop passing silently. One release closes a whole backlog of gaps across mail, storage, scheduling and the argument contract.
Fact.Mail.Received. Small attachments (up to 1 MiB by default) ride inline on the event as base64; mid-size ones (up to 8 MiB) are offloaded to object storage and the event carries a storage: {bucket, key} reference; larger ones stay metadata-only. Every attachment always carries its SHA-256, so a consumer can verify an offloaded object end to end.watch_dir: is polled (every 30 s by default) and each new file is announced as Fact.Data.FileDiscovered — a first-class trigger for ingest pipelines. The first listing announces the existing backlog, so nothing that landed during downtime is missed.data.upload accepts body_json — store ${prev.rows} (or any structured value) verbatim as JSON, no manual transform step.scheduler.register_schedule takes interval_seconds (or cron_expr: "@every 45s") for “every N seconds/minutes” cadences that cron cannot express — anchored so the rhythm never drifts.get-page tool so an agent can read back the page it just published. IMAP login failures now name the user, host, port and the usual causes.# React to every file a supplier drops into a watched folder
name: ingest-supplier-files
trigger:
event: Fact.Data.FileDiscovered
filter: { bucket_id.eq: supplier-edi }
steps:
- run: data.download
with: { bucket: supplier-edi, key: "${trigger.key}", as_b64: true }
Released 2 July 2026 — Hotfix — watched folders on every backend.
v0.5.0-beta.17 is a same-day hotfix for watched folders. The beta.16 folder watcher only started its poller for SFTP buckets; buckets on S3/MinIO, FTP or a local directory registered a watch that never fired. Now every backend with a watch_dir: gets its poller.
watch_dir: on an S3/MinIO, FTP or local-filesystem bucket now spawns the polling task exactly as an SFTP one always did — the beta.16 listing engine was already backend-agnostic; only the start-up gate was stuck in the SFTP-only past.Released 2 July 2026 — Bucket registry in the vocabulary (46 → 48) + contract truth.
v0.5.0-beta.18 grows the vocabulary to forty-eight and makes the reference material provably true. Storage buckets can now be registered from playbooks — with a self-healing boot pattern to match — the validator understands multi-document files, and a sweep of the argument contract fixed every place where the documentation, the validator and the running daemons disagreed.
data.register_bucket and data.unregister_bucket. Binding a storage backend is now a first-class playbook step. Because the daemon's bucket registry is held in memory, the canonical pattern is a provisioning playbook on Fact.System.Boot that re-registers your buckets on every start — the registry heals itself after any restart. The vocabulary grows from forty-six to forty-eight.----separated playbooks (a provisioning + business pair, say); binions-cliconsole validate now checks each document and reports them as [doc 1], [doc 2], …register_table are case-insensitive with the usual SQL synonyms (text, varchar, decimal…) — now the published contract agrees. protocol: wss on a mailbox registration validates. And modbus.write — whose strict schema had been rejecting every valid payload at the argument gate — validates again.validate against the real contracts.# Provisioning — heal the in-memory bucket registry on every boot
name: register-storage-buckets
trigger:
event: Fact.System.Boot
filter: { component.eq: playbook-service }
steps:
- run: data.register_bucket
with:
bucket_id: mailbox-attachments
backend: minio
endpoint: "http://127.0.0.1:9000"
credentials:
access_key: ${secret.MINIO_ACCESS_KEY}
secret_key: ${secret.MINIO_SECRET_KEY}
A platform-wide audit shipped as seven themed releases; the vocabulary grows 48 → 64 and every identified gap is closed.
Released 2 July 2026 — Result composability — two analytics verbs (48 → 50) + flat, addressable results.
v0.5.0-beta.19 makes every result ready for the next step. Two new analytics verbs grow the vocabulary to fifty and let one database scan fan out into per-item automations, while a sweep of the result contracts adds flat, addressable fields — one row, one value, one file — so playbooks compose without index gymnastics.
analytics.filter and analytics.emit_items. analytics.filter keeps the rows matching a where written in exactly the trigger-filter grammar — both share one evaluator, so they can never drift apart. analytics.emit_items explodes a list into one Fact.Analytics.ItemEmitted per element (up to 1,000; over the cap it fails loudly rather than truncate), and a separate playbook triggers once per item with dot-path filters into it. The vocabulary grows from forty-eight to fifty.database.query takes single: first or single: one; the result fact carries a flat row{}, read as ${prev.row.total}. Zero rows is a loud error in both modes, and one also refuses more than one match.values{} (${prev.values.avg}), forecasts expose next and last, and object listings accept sort: name|mtime|size plus order and surface the winner as first{} — “the newest file” is sort: mtime, order: desc, then ${prev.first.key}. A single-register Modbus read carries its lone reading as a flat value.body_json on Fact.Mail.Received — an IoT playbook reads ${trigger.body_json.temp} directly, with no extraction step.model, matching generation — so an audit trail always shows which model produced an answer.${...} placeholders at validation time.# One scan, many automations — filter the rows, then explode the survivors
name: overdue-scan-daily
trigger:
event: Fact.Schedule.Fired
filter:
name.eq: overdue-scan-daily
steps:
- id: overdue
run: database.query
with:
table: invoices
where:
status: overdue
limit: 500
- id: vips
run: analytics.filter
with:
data: ${steps.overdue.rows}
where:
segment: vip
- id: fan_out
run: analytics.emit_items
with:
items: ${steps.vips.rows}
Released 3 July 2026 — Correctness & reactability — a full data processor, typed reads, AI failures as facts (50 → 51).
v0.5.0-beta.20 is about catching problems before they reach production, and giving playbooks a way to react to the ones that still get through. A new data-shaping verb replaces ad-hoc extraction logic, reads gain the same typed WHERE clause that writes already had, every AI failure becomes a fact you can trigger on, and a playbook that cannot possibly run is now rejected the moment it is offered to the platform instead of failing on its first real event.
analytics.derive, replaces a whole category of hand-rolled steps. It runs a named pipeline of operations — around thirty of them, covering numbers (add, round, coalesce…), strings (regex_extract, split, concat…), objects (pick, merge…) and arrays (sort, unique, join…) over a list of records, with no expression language to get subtly wrong. A pipeline that yields exactly one record gets a flat row{}, the same convenience a single-row query gets. The vocabulary grows from fifty to fifty-one.database.query, exists, count and aggregate now accept the full sixteen-operator grammar — including or: alternatives and qualified table.col.op keys for joins — and a string compared against a timestamp column gets the right datetime cast automatically, qualified or not.Fact.AI.OperationFailed, which a playbook can trigger on directly to page an operator or fail over to another provider.ai.extract fields are now genuinely typed. Declare a field as text, int, decimal, bool or date and the value is coerced to match — European or US number formats both parse correctly — and on_mismatch decides what happens when it can't: fail (the default), null, or raw.u16 through f64), a word order, and a scale/offset, and ValueChanged carries the physical value directly — not raw register words.${secret.KEY}, and more, all fail the deploy with a concrete list of errors, while the last good version keeps serving. binions-cliconsole validate --dry-run runs the same check offline before you deploy at all.- id: shape
run: analytics.derive
with:
data: ${steps.q.rows}
ops:
- { fn: regex_extract, as: invoice_no, of: subject,
pattern: "INV-[0-9]{4}-[0-9]+" }
- { fn: to_number, as: net, of: amount_raw }
- { fn: mul, as: gross, of: [net, 1.23] }
- { fn: round, as: gross, of: gross, decimals: 2 }
Released 3 July 2026 — Opening integrations — multi-instance AI, webhook multicast, named connections (51 → 53).
v0.5.0-beta.21 opens the platform up to more of the systems you already run. The AI broker now talks to as many providers as you configure at once, a webhook call can pull structured data back into a playbook instead of only pushing it out, one send can fan out to a whole list of registered endpoints, edge routes gain real access control, and a playbook can open and use a second database connection by name.
[providers.instances.<name>] blocks as you need — a local, keyless model next to a hosted one — and every ai.* action routes with provider: <name>. One OpenAI-compatible client type covers OpenAI, OpenRouter, DeepSeek, Ollama, vLLM and LM Studio, plus a Gemini preset; model is now optional, falling back to the instance's own default.ai.usage_report and database.register_connection. Every AI call is now metered — calls, tokens, cost — per provider and model, all-time and by the day, and ai.usage_report reads that ledger back, including “what has this run cost so far.” database.register_connection lets a provisioning playbook open a second, named database connection at boot — even a second Postgres server — the same self-healing pattern already used for storage buckets. The vocabulary grows from fifty-one to fifty-three.expect_json: true turns an API call into a data source: the JSON body comes back as response_json{}, ready to interpolate, with the response headers alongside it.endpoints:, choose parallel or sequential, and a summary fact closes the group once every member has answered — useful for fanning one alert out to every on-call channel you have.[providers.instances.primary]
type = "anthropic"
key_file = "/opt/binions/aiinjector-service/secrets/anthropic.key"
default_model = "claude-sonnet-4-5"
[providers.instances.local]
type = "openai_compatible"
base_url = "http://127.0.0.1:11434/v1"
default_model = "llama3.1:8b"
Released 3 July 2026 — Industrial alarms, a writable Modbus server, and a universal file bus (53 → 56).
v0.5.0-beta.22 takes the platform deeper into industrial and file-based processes. Modbus subscriptions can now raise and clear alarms on their own, the Modbus daemon can act as a slave device that other masters write to, storage backends widen to include an application database and a write-only ingest endpoint, and schedules can fire at an exact instant instead of only on a recurring grid.
thresholds[] and a deadband on a Modbus subscription and the platform runs a per-register alarm state machine for you: Fact.Modbus.ThresholdCrossed fires once when a value crosses in, and once when it recedes back out past the deadband — never a flood of facts while a value hovers near the line.Fact.Modbus.ServerRegisterWritten, so an inbound industrial write is just another trigger. The new modbus.set_server_registers verb is the other direction — the platform computes a value and a SCADA system reads it. A siemens vendor profile also sets the classic low-word-first decoding convention by default, though an explicit word_order always wins.data.move and data.copy. A universal file mover means any pair of registered backends now composes — including a new MongoDB GridFS backend and a write-only HTTP-ingest backend for POSTing straight into an ingest API. The vocabulary grows from fifty-three to fifty-six.at: fires a schedule once at an exact instant — useful for a due date read out of a database row — and dates[] gives it a whole calendar of instants. Both auto-deregister once exhausted, and an explicit timezone keeps a cron schedule on the same local hour through both daylight-saving transitions.read_subscriptions:
- spec: "holding/100:2"
decode:
type: f32
scale: 0.1
deadband: 0.5
thresholds:
- { op: gt, value: 80.0, label: temp_high }
- { op: lt, value: 5.0, label: temp_low }
Released 3 July 2026 — A workable mailbox — folders, templated mail, and threading (56 → 61).
v0.5.0-beta.23 turns the mailbox into a workable filing cabinet, not just an inbox you listen to. A playbook can now list folders, pull messages from any of them on demand, move and flag what it has processed, and reply inside the original thread with a proper template — while, separately, the page server's asset pipeline gets a fix that makes uploaded files actually appear where they belong.
mail.list_folders, mail.fetch, mail.move, mail.mark, mail.append. mail.fetch explodes a folder on demand into the exact same Fact.Mail.Received shape the live listener produces, so a pull-based playbook and a push-based one can share the same downstream steps; it closes with a summary Fact.Mail.Fetched. mail.move files a processed message into another folder, mail.mark flags it seen or answered, and mail.append stores a message without sending it. Every failure here is a loud Fact.Mail.OperationFailed. The vocabulary grows from fifty-six to sixty-one.mail.send now renders a template. Give it template_text or template_html plus a vars{} map instead of a pre-built body, and loops and conditionals live in the template itself. An undefined variable fails loudly rather than sending half a message — a broken invoice template can't quietly reach a customer.in_reply_to and references on mail.send thread a reply properly, so a customer sees one conversation, not a stack of disconnected messages.topic_pattern like sensors/+device/+metric on a mailbox registration and every message carries its captured segments as topic_params{} — filter on topic_params.device.eq with no manual string-splitting.[retention]) takes over rotation, ageing and pruning of the event log, with the same defaults the old external logrotate job used.- id: pull
run: mail.fetch
with:
alias: crm-inbox
folder: "Faktury"
unseen_only: true
limit: 20
Released 3 July 2026 — The data plane — documents, links, AI economics, composable transactions (61 → 64 verbs).
v0.5.0-beta.24 rounds out the data plane. A PDF or DOCX parses inside the platform, a stored report becomes an expiring download link, the AI verbs gain fallback chains, a fail-closed cost cap and a 200-item batch mode, database transactions compose parent-and-child inserts atomically, and reads paginate. Three new verbs bring the vocabulary to sixty-four.
data.parse — documents to text, in-platform. Lifts the text layer out of a stored PDF (no OCR — a scan fails loudly with no_text_layer rather than returning garbage), reads DOCX, and passes plain text through; format auto-detects. The missing middle link of “an invoice arrives as a PDF attachment” — offload → parse → extract → persist — now closes without leaving the platform.data.presign — share a link, not an attachment. Turns an object into a time-limited download URL (default one hour, up to seven days) on S3/MinIO backends; others refuse loudly.ai.batch and AI economics. Classify or extract up to 200 texts in one step: one aggregate Fact.AI.BatchCompleted with per-item outcomes — a single bad item never kills the batch — and a running cost cap that stops a runaway bill. On every AI verb, providers: [a, b] chains fallbacks (a provider error falls through to the next) and max_cost_usd pre-flights the spend, refusing to run when the model has no known price.database.transaction, a later operation references an earlier write’s generated columns as "@tx:<op>.<column>" — an order and its lines land atomically, foreign keys included (PostgreSQL and SQLite).query/aggregate gain offset (pagination) and query gains distinct; anomaly detections now carry the original record and its true record_index; statistics group with group_by; received mail mirrors its first attachment flat (first_attachment{}, attachments_count).body_b64 sends raw bytes, form + form_files build a multipart upload, and timeout_ms overrides the client timeout per send.# The pipeline that used to need an external parser — now three steps
steps:
- id: parse
run: data.parse
with:
bucket: ${trigger.bucket}
key: ${trigger.key} # the offloaded PDF attachment
- id: extract
run: ai.extract
with:
text: ${steps.parse.text}
providers: [primary, local] # fall back on any provider error
max_cost_usd: 0.05 # refuse to run over budget
fields:
- { name: total, type: decimal }
- { name: sender, type: text }
- run: database.write
with:
table: invoices
row:
total: ${steps.extract.result.total}
sender: ${steps.extract.result.sender}
Released 3 July 2026 — Control plane & edge — or:, governor, signed webhooks, verified writes (vocabulary stays 64).
v0.5.0-beta.25 hardens the control plane and the edge — and closes a long programme. Trigger filters gain their one deliberate grammar addition (or:), a declarative governor tames event storms, inbound webhooks verify signatures, live pages re-render in place, industrial writes read themselves back, and the operator CLI grows friendlier. The verb count stays at sixty-four: this release is arguments, grammar and configuration — the finishing pass of a platform-wide capability review.
or: in trigger filters. The reserved key holds a flat list of condition branches; the filter matches when every top-level condition matches and at least one branch does. One triage playbook replaces three copies that differed only by filter. Deliberately flat — no nesting, no not; genuinely independent shapes still read best as separate playbooks.debounce_ms drops a matching fact that arrives too soon after the previous spawned run, and max_concurrent caps in-flight instances — extra facts are dropped, not queued, so a mail storm never becomes an hour-long backlog. Manual triggers and replays bypass both./in gateway can require an HMAC-SHA256 signature over the raw body (Stripe/GitHub style), configured per route with the secret in a root-owned file that is re-read on every request — rotation without a restart. A bad or missing signature is answered 401 and audited as Fact.Http.Rejected; it never reaches a playbook.show.update_page accepts template + data: re-render a registered template at the page’s own registered path, hot cache included — refreshing a dashboard no longer hard-codes an output path.modbus.write verify: true reads the written range back and fails loudly on a mismatch; scheduler.register_schedule jitter_secs spreads a shared cron line deterministically across a fleet.path_prefix (with optional strip_prefix) and weighted backends: [ { url, weight } ] — path-scoped publishing and load-balanced canaries from one playbook step.[alerting] emits one Fact.Logs.ErrorRateExceeded when a daemon starts erroring hard; binions-cliconsole --service <daemon> resolves the right event-bus connection from the daemon’s own config, and ls events --follow tails facts live.# One triage rule instead of three — with flood control built in
trigger:
event: Fact.Mail.Received
filter:
via.eq: crm-inbox
or:
- subject.contains: "faktura"
- subject.contains: "invoice"
- { from.email.endswith: "@accounting.example", has_attachments: true }
debounce_ms: 2000
max_concurrent: 4