Using tables in workflows

A table is a resource: structured rows your workflows read and write. A Table block is the step that does the reading and writing inside a workflow. You pick an operation on the block (query rows, insert a row, update rows), point it at a table, and its result is remembered under the block's name for later blocks to use.

A workflow uses whichever Table operations its task needs. It might read rows to work on, write rows produced elsewhere, update rows in place, or just look something up mid-run. This page covers the operations and shows a few ways to combine them.

Throughout this page the running example is a leads table with columns company, email, description, status, category, and score. The goal is to find unprocessed leads, have an Agent classify each one, and write the category back.

The Table block

A Table block performs one operation against one table. The Operation dropdown picks the action; the Table selector picks the target. The fields below those two change based on the operation you choose.

The operations fall into three groups:

  • Read: Query Rows, Get Row by ID, Get Schema.
  • Write: Insert Row, Batch Insert Rows, Upsert Row.
  • Update and delete: Update Row by ID, Update Rows by Filter, Delete Row by ID, Delete Rows by Filter.

Every row carries three built-in columns alongside your own: id (the row's unique identifier), createdAt, and updatedAt. Sim manages these for you, so you never include them when inserting. You can still filter and sort on them.

Reading rows

Query Rows retrieves rows from a table, with optional filtering, sorting, and pagination. It's how a workflow gets its input from a table.

For our example, the block queries leads where status equals unprocessed. Its output holds the matching rows plus counts:

{ success: true, rows: [ { id: "row_...", company: "Acme", ... } ], rowCount: 5, totalCount: 42 }

Later blocks read these by name: <table1.rows> is the array, <table1.rowCount> is how many came back, <table1.totalCount> is how many matched the filter before the limit. (For more on reading outputs by reference, see how blocks pass data.)

Logs
Start9ms
table184ms
OutputInput
rowsarray
0object
idstring
companystring
"Acme"
statusstring
"unprocessed"
rowCountnumber
5
totalCountnumber
42

Filter narrows the result. You can build rules visually - pick a column, an operator, and a value - or write the filter directly as a predicate. One condition names a field, an operator, and a value:

{"field": "status", "op": "eq", "value": "unprocessed"}

Combine conditions with all (AND) or any (OR), and nest the groups for mixed logic:

{"all": [
  {"field": "status", "op": "eq", "value": "unprocessed"},
  {"field": "createdAt", "op": "gte", "value": "2026-06-01"}
]}

Order sorts the result as a list of column/direction pairs, for example [{"field": "createdAt", "direction": "desc"}]. Columns to Return narrows each row to the fields a downstream step actually needs. Limit caps how many rows come back per page, and Cursor continues a previous page - see Paginate large reads below.

For a one-off point lookup, use Get Row by ID with a single Row ID. Get Schema returns the table's column definitions, useful when a workflow needs to inspect structure before writing. The full operator list lives in the Table block reference.

Writing rows

Insert Row adds one row. Its Row Data is an object whose keys match your column names:

{ company: "Acme", email: "deals@acme.com", description: "...", status: "unprocessed" }

The output is the inserted row, including the id and timestamps Sim generated for it.

Batch Insert Rows adds many rows in one operation (up to 1000) from a Rows Data array. Use it instead of looping Insert Row when you have a set of results to load at once. Its output reports insertedCount.

Upsert Row inserts a row, or updates the existing one if it matches a unique column. Its output includes an operation field set to insert or update, so a later block can tell which happened.

Row data must match the table's columns and types. A number column rejects "twenty"; a boolean column wants true, not "true". If an Agent produces the value, give it a structured output so the shape is predictable before it reaches the table.

Updating rows

To change an existing row you either name it or filter for it.

Update Row by ID modifies one row. It takes a Row ID, often <table1.rows[0].id> from an earlier query, and Row Data with only the fields you want to change. Unlisted fields stay as they were.

Update Rows by Filter applies the same update to every matching row. Use it for a bulk change, such as marking a selected batch for review. For a different classification per lead, iterate over the queried rows and use Update Row by ID for each result. The filter operation reports updatedCount and updatedRowIds.

Delete Row by ID and Delete Rows by Filter remove rows the same two ways, by ID or by filter, and report a deletedCount. Deletes are mostly for cleanup, not part of the everyday read-process-write loop.

Example: enriching rows

One way to combine the operations is to query rows, process them, and write the results back. Here, the workflow classifies unprocessed leads:

  1. Table (Query Rows) reads leads where status is unprocessed.
  2. Add a For Each Loop over the returned rows array.
  3. Inside the loop, an Agent reads the current row and returns a structured output, such as { category: "enterprise", score: 0.9 }.
  4. Table (Update Row by ID) uses the current row's ID to write that result and update its status.

After the run, the table holds the enriched rows. The next run queries them again, and the status column keeps the workflow from reprocessing what it already handled. So the table serves as both the queue the workflow pulls from and the record of what it has already done.

Variations

Lookup mid-run. A Table block doesn't have to be the first or last step. Place a Query Rows in the middle to fetch reference data while processing: query a pricing table by the order's currency, then let the Agent use the result to compute a total.

Iterate row by row. Wrap a Query → process → update cycle in a Loop block to handle one row at a time. This runs sequentially, slower than a batch update but useful when each row needs its own multi-step logic. Inside the loop the Agent reads the current row and an Update Row by ID writes its result.

Paginate large reads. Omit Limit to get every matching row in one response; the query fails if the result exceeds 5MB, so narrow with a filter rather than guessing a limit. With a Limit, a page can end at the limit or early once its rows reach the 5MB budget — so a short page does not mean the end. Pass the returned nextCursor back as Cursor and keep going while it is non-null. Stop only when nextCursor is null; never infer completion from the row count.

Inspecting reads and writes

Every Table block's input and output is recorded in logs. For a Query block, the log shows the filter and order it sent and the rows it received. For an Update or Insert, it shows the row data written and the count affected. When a write does nothing or a query comes back empty, the log is where you check the filter and the data shape before looking anywhere else.