Blocks
A block is a plain TypeScript module: named typed inputs, named typed outputs, an async process function, three scopes of persistent state, and nothing else injected. No special control-flow primitives, no proprietary expression language.
import { defineBlock } from "flowbun";
export default defineBlock({ name: "debounce", config: { ms: 30_000 }, inputs: { signal: {} as { state: string; at: number } }, outputs: { stable: {} as { state: string } }, async process({ signal }, ctx) { const last = await ctx.state.block.get<number>("lastAt"); if (last !== undefined && signal.at - last < ctx.config.ms) return; await ctx.state.block.set("lastAt", signal.at); return { stable: { state: signal.state } }; },});Firing semantics
Section titled “Firing semantics”process() fires once per message arriving at one named input port; the runtime only ever populates that one port. There are no join semantics — a block that needs cross-port memory uses its own state, exactly how debounce remembers its last timestamp.
State is three-scope SQLite (ctx.state.block, ctx.state.flow, ctx.state.global), stored in WAL mode in a single flowbun.sqlite. Because it lives outside the block’s memory, a debounce timer or “have I fired today” flag survives block reloads, flow restarts, and even kill -9.
Configuration
Section titled “Configuration”A block declares default config values in defineBlock({ config: {...} }). A flow’s wiring file can override any subset per node — defaults merge underneath, so new config fields work without touching existing wiring files.
Effects
Section titled “Effects”Ordinary blocks can import anything on npm and fetch any API — but they are never handed a Home Assistant connection. Only the @hass/trigger, @hass/action, and @hass/read boundary blocks (plus direct readEntityState()/performHassAction() calls, which relay through the flow’s single connection) can reach your house.
