Skip to main content

Variables and data

Steps pass data through variables. Templates turn those values into SQL, JSON arguments, and prompts.

Build with AI

The flow authoring guide documents every interpolation edge case (including "silent empty string" behavior).

For AI assistants

Read the flow authoring guide before generating JSON—especially LOOP ordering and object passing rules.

Syntax

SyntaxUse
{{ $.name }}Interpolate a string inside SQL, JSON text fields, or prompts.
"$.object"Pass a whole object or array through without stringifying (quoted path, no braces).
@.nameGlobal variable—readable from any later step in the run.
$.nameLocal variable—scoped to the current chain / iteration.
@.trigger.*Payload from the run's trigger — the shape depends on trigger type (see below).

Do not wrap whole objects in {{ }}—you will stringify unpredictably. Use "$.var" instead.

Trigger data by type

Each trigger type populates different fields under $.trigger (and @.trigger for globals). Only one shape applies per run.

Trigger typeAvailable under $.trigger
queryrow.<column> — columns from your SQL SELECT
activityactivity.activity_id, activity.model_id, activity.account_id, activity.user_id, activity.timestamp, activity.count
signalsignal.signal_id, signal.rule_id, signal.type, signal.account_id, signal.timestamp, plus optional signal.message, signal.attributes, signal.value, signal.previous_value
needle_moverneedle_mover.needle_mover_id, needle_mover.title, needle_mover.description, needle_mover.state, needle_mover.impact, needle_mover.label, needle_mover.created_at
conversationconversation.key, conversation.metadata, conversation.timestamp

Event-driven runs also include $.trigger.account_ids when account scope is available.

$.trigger.row is query-only

Referencing $.trigger.row when the run was started by an activity, signal, needle mover, or conversation trigger produces empty or missing values. Use the matching path for the trigger type — for example $.trigger.activity.account_id on an activity-triggered run. If you need data that is not in the trigger payload, add a semantic.query step to look it up.

Saved config vs runtime naming

Some field names differ between the saved trigger configuration and the runtime payload. For example, the saved needle mover config uses impacts (plural array) while the runtime event carries impact (singular string). See Triggers for the saved shapes and the table above for runtime fields.

Outputs (out)

FormMeaning
{ "set": "@.global" }Store result globally.
{ "set": "local" }Store locally for tight chains inside one scope.
{ "append": "@.list" }Append each result to an array (common in loops).
{ "merge": "@.obj" }Shallow-merge object keys into an existing global object.

Rule of thumb: if any later step outside the current loop or branch needs the value, use an @. global. When unsure, prefer @.—locals are easy to lose across LOOP scheduling.

Templates

String fields go through Go text/template semantics:

  • Conditionals: {{if $.limit}}LIMIT {{ $.limit }}{{else}}LIMIT 100{{end}}
  • Indexing arrays: {{ index $.rows 0 }} — bracket syntax like $.rows[0] is not supported.
  • Missing variables become empty string (not an error), which can make SQL silently wrong—validate in testing.

Datasets

Datasets are durable key/value stores of JSON records. Read them through semantic.query using dataset_records('dataset_name') as a table.

Rules:

  • Do not invent dataset names—use names your workspace already has.
  • Verify a dataset exists (for example SELECT 1 FROM dataset_records('name') LIMIT 1) before writing.
  • Writes (upsert, set_field, delete) are destructive—only use them when the agent's purpose includes persistence.

Common patterns

  • Fan-out then aggregate: use BRANCH/JOIN or sequential CALLs writing to distinct @. keys, then an AGENT step that reads multiple globals.
  • Incremental processing: keep a dataset of processed keys; query with LEFT JOIN to skip completed rows (see Examples).