Elizon Docs
Data Tables

Worked example: extract once, check daily

A contract register built from two workflows — one that writes it, one that watches it.

This is the pattern most Data Tables end up being: extract once, check daily. Somebody uploads a contract, a run reads it, a person confirms it, and the result becomes a row. From then on a schedule watches the register and emails whoever needs to act.

Two workflows, one table. The table is what connects them — neither workflow knows the other exists.

The table

Create it in Studio → Data Tables, or with a Table · Create table node that has Return existing if it exists ticked so the workflow is safe to re-run.

ColumnTypeHolds
ContractstringThe contract reference. The key.
CounterpartystringWho it is with.
DeadlinedateThe date something has to happen by.
OwnerstringThe email address that gets the reminder.
Statusstringopen while it still matters.

Contract is what makes the whole pattern work: it is the value the upsert matches on, so the same contract uploaded twice updates one row instead of making two.

Workflow 1 — write the register

Form Trigger — the upload

A Form trigger with a file field named contract and a text field named owner. The run starts when somebody submits it, and the uploaded file arrives as {{ trigger.body.contract }}.

AI Prompt — the extraction

An AI Prompt node with the uploaded file attached, asked for the three facts the register needs as JSON:

Read the attached contract and answer with JSON only:
{"contract": "<reference>", "counterparty": "<name>", "deadline": "<YYYY-MM-DD>"}

Ask for the deadline as YYYY-MM-DD. A date column accepts ISO 8601 and nothing else, so "next Tuesday" is a failed node rather than a wrong row.

Human Review — the confirmation

A Human Review node, assigned to whoever owns contracts, with three response fields: contract (text), deadline (date) and counterparty (text). Put what the model read into the node's message, so the reviewer is checking a claim rather than starting from a blank form:

Extracted from the upload: {{ upstream.extract-1.output.text }}
Correct anything that is wrong below.

This is the step that makes an extraction trustworthy enough to keep. What the reviewer submits is what gets written: {{ upstream.<review node id>.output.response.deadline }}.

Data Table — the upsert

A Row · Upsert row node on the register. Its conditions are the key, and its mapping is the row:

node data — Row · Upsert into the register
{
  "resource": "row",
  "operation": "upsert",
  "tableId": "8a9f0e42-1d7c-4a3a-9c2e-6b4d1f77e001",
  "tableName": "Contract register",
  "conditions": [
    {
      "id": "cond-1",
      "column": "Contract",
      "operator": "eq",
      "value": "{{ upstream.review-1.output.response.contract }}",
      "valueMode": "expression"
    }
  ],
  "conditionMatch": "all",
  "mapping": "manual",
  "mappingRows": [
    {
      "id": "map-1",
      "column": "Contract",
      "value": "{{ upstream.review-1.output.response.contract }}",
      "valueMode": "expression"
    },
    {
      "id": "map-2",
      "column": "Counterparty",
      "value": "{{ upstream.review-1.output.response.counterparty }}",
      "valueMode": "expression"
    },
    {
      "id": "map-3",
      "column": "Deadline",
      "value": "{{ upstream.review-1.output.response.deadline }}",
      "valueMode": "expression"
    },
    {
      "id": "map-4",
      "column": "Owner",
      "value": "{{ trigger.body.owner }}",
      "valueMode": "expression"
    },
    { "id": "map-5", "column": "Status", "value": "open", "valueMode": "fixed" }
  ]
}

Run it twice with the same contract reference and the register holds one row: the node's output says "action": "inserted" the first time and "action": "updated" the second.

Tick Dry Run while you are still wiring this up. The node reports what it would write — and for an upsert that would insert, the exact cells, as result.insertValues — without touching the register.

Workflow 2 — watch the register

Recurrence — daily

A Recurrence trigger set to run once a day. A scheduled run carries no payload, so everything this workflow needs comes out of the table.

The cutoff date

{{ }} is dot-path substitution, not an expression language: there is no arithmetic in it, so today + 30 days has to be produced by a node. An AI Prompt node asked for exactly that is the simplest version:

Answer with today's date plus 30 days, as YYYY-MM-DD, and nothing else.

Its output is then {{ upstream.horizon-1.output.text }}.

Data Table — what is coming due

A Row · Get row(s) node: everything still open whose Deadline is at or before the cutoff, soonest first.

node data — Row · Get what is coming due
{
  "resource": "row",
  "operation": "get",
  "tableId": "8a9f0e42-1d7c-4a3a-9c2e-6b4d1f77e001",
  "tableName": "Contract register",
  "conditions": [
    { "id": "cond-1", "column": "Status", "operator": "eq", "value": "open", "valueMode": "fixed" },
    {
      "id": "cond-2",
      "column": "Deadline",
      "operator": "lte",
      "value": "{{ upstream.horizon-1.output.text }}",
      "valueMode": "expression"
    }
  ],
  "conditionMatch": "all",
  "orderBy": [{ "id": "sort-1", "field": "Deadline", "direction": "asc" }],
  "returnAll": true
}

conditionMatch is all: both have to hold. Switch it to any and you would email about every open contract and every past deadline, which is not the same question.

Send Email — the reminder

A Send Email node. The rows are {{ upstream.due-1.output.result.rows }}, and how many there were is {{ upstream.due-1.output.affected }}.

To send nothing on a quiet day, put an If / Else node in front of it and branch on {{ upstream.due-1.output.affected }} — or use a Row · Row exists node, whose {{ upstream.<id>.output.result.exists }} is a plain boolean.

What this pattern gets you

  • The register outlives the run. A contract read in October is still found by the schedule in November.
  • Corrections stick. A deadline fixed by hand in Studio → Data Tables is what the next day's Get row(s) reads. Nothing re-extracts over the top of it.
  • Re-uploading is safe. The upsert's key makes the second upload an update.
  • The two workflows stay independent. Replace the extraction with a better one, or the email with a Slack message, and the other half does not change.