Working with data: tables and imports
Custom Tables are your own data tables inside Routario — invoices, shipments, products, support tickets, whatever your operation tracks. Flows can read rows, write rows, and loop over them. You define the columns; Routario stores and queries the data.
This guide covers creating tables, getting data in, and using them from automations.
What Custom Tables are for
Section titled “What Custom Tables are for”A Custom Table is a structured dataset you own inside Routario. You define its columns (text, number, date, and so on) and your flows populate and query it. Think of it as a lightweight spreadsheet that your automations can read and write without you touching it.
Use Custom Tables for data that Routario doesn’t already model — shipment statuses, internal product catalogues, incident logs, pricing sheets, delivery schedules. For contacts, companies, or deals, use the dedicated Contacts features instead.
Creating a table
Section titled “Creating a table”Go to Settings → Reference Data → Tables and create a new table. You have two starting points:
From a file. Upload a CSV or spreadsheet and Routario infers the columns from the header row. This is the fastest way to start — paste in an existing export and you’re done.
Blank. Add columns one by one and choose the type for each. Useful when you’re designing the schema from scratch before any data exists.
Getting data in
Section titled “Getting data in”Import a batch from a spreadsheet
Section titled “Import a batch from a spreadsheet”The Sync to Table step upserts a list of rows into a table in one operation. Give it a key column — a column whose value uniquely identifies a row, like an order number or product code — and it will update matching rows and insert new ones. Re-running the same flow with the same data produces no duplicates.
This is the right tool for a nightly or hourly sync: pull a fresh export, run it through Sync to Table, and your table stays current.
Sync to Table is column-tolerant: if your source has extra columns your table doesn’t define, they’re quietly dropped and listed in the dropped_keys output — useful for catching mapping mistakes. One bad row never aborts the batch; errors are counted and skipped.
Read rows from a spreadsheet first
Section titled “Read rows from a spreadsheet first”If the data comes in as an email attachment or uploaded file, add a Read spreadsheet step before Sync to Table. It turns an .xlsx, .xlsm, or .csv file into a list of rows keyed by the header row — rows[0].item_code, rows[0].quantity, and so on. Pass the rows output straight to Sync to Table.
Sync events from a calendar feed
Section titled “Sync events from a calendar feed”Calendar (iCal) fetches any iCalendar feed (Google Calendar, Outlook, Eventbrite, booking systems — anything with an .ics or webcal link) and returns its events as structured rows. Each event has a stable uid. Pair it with Sync to Table using key_column = uid and you get an idempotent calendar-to-table sync: re-run any time and only new or changed events land.
Schedule trigger (daily) → Calendar (iCal) url = your feed link, since = now, until = +30d → Sync to Table table = delivery_schedule, key_column = uidLand extracted documents into a table
Section titled “Land extracted documents into a table”When the data arrives as a document — an emailed invoice, a scanned delivery note — rather than a spreadsheet, you can pull its fields out and land them straight into a table. Extract reads an unstructured file into typed fields using a recipe; Ingest into Table writes those fields into the table the recipe is bound to, recording which document they came from and updating the matching row instead of duplicating when the same document arrives twice. If a document is low-confidence or doesn’t look right, Flag for review parks it on your inbox for a person to confirm or dismiss — nothing is written on a guess. Bind a recipe to the table once (Table → Ingestion recipe) and every flow just names the table.
Reading data in a flow
Section titled “Reading data in a flow”The Find records step returns rows from a table. Set a filter to match on a column value — { "status": "pending" } — or leave it empty to get all rows (up to the limit, which defaults to 100). The records output is a list you can pass to a For each loop.
Find records outputs a found flag. Wire your flow on it: if found is true, loop over the records and act on each; if false, send a notification or stop early. This is the standard pattern for “find matching rows, then do something per row.”
Writing from a flow
Section titled “Writing from a flow”Three steps cover single-row writes:
Create Record adds one row and returns its record_id. By default it always inserts — use it to accumulate data across runs, like logging an event, recording an incoming lead, or appending a metric. Turn on Update if it already exists to update a matching row instead of duplicating it — see Update or insert a single row below.
Update Record patches an existing row by record_id. Only the columns you include in values change; everything else is untouched. Use it to change a status field, bump a counter, or fill in a column a later step computed — reach for it when you already hold the row’s id (say, from a Find records loop).
Get Record fetches one row by record_id — useful when you already hold the id and want to re-read the current values before deciding whether to update. Returns a found flag the same way Find records does.
Update or insert a single row
Section titled “Update or insert a single row”Often a flow needs to keep one row current — one record per order, per customer, per sprint — and re-run without piling up duplicates. That’s an upsert: update the row if it’s already there, insert it if it isn’t.

Create Record does this in one step. Turn on Update if it already exists and pick a Match on column — the column whose value identifies the row, like order_number or cycle. On each run:
- If a row already has that value, Create Record merges your new values into it: the columns you provide win, and columns you leave out keep their old values.
- If no row matches, it inserts a new one.
The created output tells you which happened — true for a fresh insert, false for a merge — so a later step can branch on “was this new?”.
Worked example — one row per order from a webhook. An incoming webhook sends an order update, and you want your orders table to hold exactly one row per order_number, refreshed each time an update arrives:
Incoming webhook → Create Record table = orders Update if it already exists = on Match on column = order_number order_number = {{ payload.order.number }} status = {{ payload.order.status }} total = {{ payload.order.total }}Re-send the same order and its one row updates in place — no duplicates. This is the single-row counterpart to the batch sync above: Sync to Table upserts a whole list of rows keyed by a column; Create Record upserts one.
Worked example: daily shipment sync and action flow
Section titled “Worked example: daily shipment sync and action flow”This example uses two automations. The first keeps the table fresh; the second acts on today’s rows.
Automation 1 — nightly import
Section titled “Automation 1 — nightly import”- Schedule trigger. Set to run every night at 01:00.
- Read spreadsheet. Point it at the shipments export file (delivered earlier by email or placed in a known path). It outputs a
rowslist. - Sync to Table. Table =
shipments, rows ={{ read_spreadsheet.rows }}, key column =shipment_id. Rows are upserted; nothing duplicates on re-run.
Automation 2 — morning action run
Section titled “Automation 2 — morning action run”- Schedule trigger. Set to run every morning at 07:00.
- Find records. Table =
shipments, filter ={ "dispatch_date": "{{ now.date }}", "status": "pending" }. Label this steptodays_shipments. - Branch. If
{{ todays_shipments.found }}is false, send a notification (“No shipments due today”) and stop. - For each over
{{ todays_shipments.records }}. Inside the loop:- Do whatever the shipment needs — send a carrier API call, notify a driver, post to a webhook.
- Update Record. Set
record_id = {{ loop.item.record_id }}andvalues = { "status": "dispatched" }so the row is marked done.
Recovering deleted rows and tables
Section titled “Recovering deleted rows and tables”Deleting a row or a table in Routario is reversible. Nothing is erased on the spot — deleted rows and tables are hidden and kept in a Trash, so an accidental delete (yours, a teammate’s, or an automation’s) is a quick undo rather than a lost afternoon.
Restore a deleted row
Section titled “Restore a deleted row”When you delete a row (its menu calls the action Archive row), it disappears from the table but moves to that table’s Trash.
Open the table and look for the Trash button in the top-right actions — it shows a count when there’s anything to recover. Click it to see the deleted rows, each with the columns it had and when it was deleted. Hit Restore on a row and it returns to the table exactly as it was.
Restore a whole table
Section titled “Restore a whole table”Deleting a table hides it and its rows, but keeps every row intact underneath. To bring one back, go to Settings → Reference Data → Tables and click the Trash button there. You’ll see the deleted tables, each showing how many rows come back with it. Restore returns the table and all of its data.
See who deleted what
Section titled “See who deleted what”Every delete and every restore is recorded on the Logs page (Settings → Logs, in the Changes lens) with the person or automation that did it, what it touched, and how many rows — so a table that suddenly went empty has a plain answer: “a user called ‘agent’ deleted 17 rows two hours ago.” Deletes done by an automation are attributed to that automation, so you can always tell a person’s action from a flow’s.
Worked example — an automation cleared a table by mistake. You open a table and it’s empty. Rather than guessing, check the Logs page: it shows the delete, who or what did it, and the row count. Open the table’s Trash, and the rows are sitting right there — Restore them and you’re back to where you were, with a record of what happened.
Filtering, grouping, and summarizing without a flow
Section titled “Filtering, grouping, and summarizing without a flow”Everything above is about getting rows in and reading them from a flow. If what you actually want is an ad-hoc breakdown — “cars by color, last 24 hours” — you don’t need either: build a saved view directly on the table. See Filter, group, and summarize a table for filtering, grouping, real totals over the full table, and reusing the same view as a dashboard chart.
Custom Tables over the API
Section titled “Custom Tables over the API”Custom Tables are also readable and writable over the Routario API. Use the API to push rows from an external system, pull a table’s contents into another tool, or build a lightweight integration without building a full automation.
Where to go next
Section titled “Where to go next”- Building an automation — triggers, steps, data flow, and the canvas explained from scratch.
- Filter, group, and summarize a table — saved views: filter, group-by, real totals, and dashboard charts, no flow required.
- Create Record reference — every field, including the upsert toggle and match column.
- Sync to Table reference — full field documentation for batch upserts, key columns, and error handling.
- Routario API — read and write tables from outside Routario.