Two API Modes
Schema-Driven API (Recommended)
Define your output shapes as Zod schemas and let Smithers handle everything: database creation, table generation, and Drizzle configuration.- Each Zod schema is converted to a Drizzle
sqliteTableviazodToTable(). The table name is the snake_case version of the key (e.g.,discoverstaysdiscover,researchResultbecomesresearch_result). - Standard columns
runId,nodeId, anditerationare prepended automatically with a composite primary key of(runId, nodeId, iteration). - A SQLite database is created at
./smithers.db(or the path you specify with{ dbPath: "..." }). CREATE TABLE IF NOT EXISTSstatements are executed for each table, including aninputtable that stores the run input as JSON in apayloadcolumn.- A schema registry maps string keys (like
"discover") to their Drizzle tables and Zod schemas, so<Task output="discover">resolves at runtime.
ctx.input is the decoded payload object (not the raw input row).
Zod-to-column mapping:
| Zod Type | SQLite Column |
|---|---|
z.string(), z.enum(), z.literal() | TEXT |
z.number() | INTEGER |
z.boolean() | INTEGER (boolean mode) |
z.array(), z.object(), unions, complex | TEXT (JSON mode) |
Manual API
For full control, provide your own Drizzle database instance with manually defined tables.Required Columns
All output tables must include these columns, regardless of which API mode you use:| Column | Type | Purpose |
|---|---|---|
runId | text("run_id") | Links the row to a specific workflow run. |
nodeId | text("node_id") | Links the row to a specific task node. |
iteration | integer("iteration") | Distinguishes loop iterations for Ralph tasks. Defaults to 0 for non-loop tasks. |
runId, nodeId, and iteration before inserting.
Primary Keys
The primary key convention determines how Smithers identifies whether a task has already produced output:- Non-loop tasks:
PRIMARY KEY (run_id, node_id)— or includeiterationdefaulting to 0. - Ralph loop tasks:
PRIMARY KEY (run_id, node_id, iteration)— required so each iteration produces a distinct row.
(run_id, node_id, iteration) for all tables is the safest choice. The schema-driven API does this automatically.
Reserved Schema Keys
Two schema keys have special meaning:| Key | Purpose |
|---|---|
schema.input | The workflow input table. Smithers inserts one row per run with the provided input data. The runId field is set as the primary key. |
schema.output | The workflow output table. When a run finishes, Smithers queries this table for all rows matching the runId and returns them as the run result. |
schema.output is not defined, the run result’s output field will be undefined.
Output Validation
Every output — whether from an agent or a static payload — is validated against the Drizzle table schema before being written. The validation checks:- All
notNullcolumns have values. - Column types match (text columns get strings, integer columns get numbers).
- The composite key columns (
runId,nodeId,iteration) are present and match the current execution context.
Internal Tables
Smithers automatically creates and manages these tables with the_smithers_ prefix. You never need to define or query them directly, but they are available for debugging.
| Table | Primary Key | Purpose |
|---|---|---|
_smithers_runs | (run_id) | Run metadata: status, workflow name, timestamps, config, errors. |
_smithers_nodes | (run_id, node_id, iteration) | Current state of every task node per run. |
_smithers_attempts | (run_id, node_id, iteration, attempt) | Individual execution attempts with timing, errors, and response text. |
_smithers_frames | (run_id, frame_no) | XML snapshots of the tree at each render cycle. |
_smithers_approvals | (run_id, node_id, iteration) | Approval requests and decisions. |
_smithers_cache | (cache_key) | Cached task outputs keyed by content hash. |
_smithers_tool_calls | (run_id, node_id, iteration, attempt, seq) | Tool call logs for agent tasks. |
_smithers_events | (run_id, seq) | Ordered event stream for the run. |
_smithers_ralph | (run_id, ralph_id) | Ralph loop iteration state and done flag. |
CREATE TABLE IF NOT EXISTS when runWorkflow is called. They do not require Drizzle migrations.
Upsert Behavior
Output rows are upserted — if a row with the same(runId, nodeId, iteration) already exists, it is replaced. This means:
- Re-running a task (after a revert or invalidation) overwrites the previous output.
- Retries that eventually succeed replace any partial state.
- You never see duplicate output rows for the same task in the same iteration.