df.to_sql(...) in a Python node: deliveries are authorized per invocation,
frozen against the exact data version you approved, committed in a single
destination transaction, safe to retry, and receipted.
When to use what
Native delivery is the default bulk path. Python writebacks are the
extensibility escape hatch, not a replacement for it.
The contract
A writeback node declares everything up front, and the platform enforces it:secret_keyreferences a stored tenant secret whose value is thepostgres://connection string. Credentials are resolved inside the worker at delivery time — they never appear in the definition, in receipts, or in error messages.- Exactly one table input. The writeback delivers one node’s output; shape it upstream.
mode— how rows land (below).upsertrequiresmerge_keys;appendandreplaceforbid them.schema_policy—strictfails the delivery on any column drift;additiveadds new upstream columns to the destination (inside the same transaction) and tolerates destination-only columns.
Delivery modes
append inserts every delivered row. upsert updates destination
rows matching the declared merge_keys and inserts the rest — deterministic:
a staged batch with NULL or duplicate merge keys fails whole, never partially.
replace makes the delivered input the complete destination state.
Replace is destructive by nature, so it is explicitly guarded: a replace-mode
writeback must declare requires_permission — a server-enforced
permission checked against whoever invokes it (and re-checked on retries).
You cannot author an ungated replace.
All three modes commit in one destination transaction — including
first-delivery table creation and additive column adds. A failure of any kind
rolls the destination back to exactly its prior state. The table object is
never dropped or swapped: your indexes, constraints, defaults, triggers, and
RLS policies survive every delivery, including replace.
Frozen inputs and safe retries
At invocation time the delivery freezes an execution envelope: the exact source artifact (pinned to its DuckLake snapshot version), the definition, and the invoking principal. Retries redeliver that — never a newer version of the data, never a re-resolved definition. On the destination side an idempotency ledger (_panels_staging._panels_writeback_ledger) commits
atomically with your rows, so a retry after an uncertain outcome reports the
recorded result instead of applying a duplicate effect.
Receipts
Every terminal delivery produces a receipt visible in run history and via thecore_list_writeback_outcomes MCP tool: mode, destination (schema.table —
never connection details), rows attempted / inserted / updated / deleted /
committed, columns added by schema evolution, the frozen source version, and
— on failure — a structured category (connection, permission,
constraint, schema, data, config, source) with a sanitized message.
The full per-attempt audit trail is sealed in an effect-log artifact linked
from the receipt.
Invoking a delivery
Writebacks are invoked, never run: they don’t participate in compute cascades, andRun affordances deliberately reject them. Invocation paths:
- a threshold trigger on the upstream node (e.g.
artifact.row_count > 0) firing the writeback as a service principal, - an action chain step,
- an agent calling the
core_invoke_writebackMCP tool.
Supabase specifics
Supabase is Postgres — ordinary table publishing needs nothing Supabase-specific. Two practical notes:- Use the direct connection string (or a session-mode pooler) as the secret value. Transaction-mode poolers (port 6543 by default) break single-transaction semantics.
- Deliveries connect as the database role in your connection string and are not subject to your anon-key RLS policies. Keep RLS enabled for your app’s client-side access; the writeback writes through the direct role.