Skip to content

A version 4 sync job is a graph of named nodes: requests fetch data, transforms reshape it, and destination nodes send it to a webhook or a datastore. This page lists every node type and field the V4 runtime accepts.

Skip the reading — fast path for coding agents
CLI
truto sync-jobs create --stdin < job.json
Skill
View truto-operator skill on GitHub Cursor / Claude Code: /skill truto-operator

The job

Field Required What it does
default_runtime_version Yes 4. Always send it: a job created without it runs on version 2.
label No A name for the job. Defaults to Untitled.
integration_name No The integration the job is for, or "" for a job any integration can use.
resources Yes The nodes, as an array. A run of a job with no nodes is refused with sync_job_no_resources.
args_schema No The arguments a run takes. See Args.
args_validation No A JSONata expression over args. Return null to accept the run, or { "message": "..." } to refuse it with 400 and that message.
mutex_key No A placeholder template, such as {{args.integrated_account_id}}. While a run with the same resolved key is still active, a new run is refused with 400 and truto_error_code: sync_job_run_not_completed.
state_key No A placeholder template that names where get_state and update_state keep their values. Runs that resolve to the same key share state. A state node in a job without a state_key fails the run.
delay_between_requests_ms No A pause between requests.

Args

A V4 run gets everything it needs from args: which account to read, and where to send the output. The run itself has no integrated_account_id, webhook_id or datastore_id. Truto ignores those top-level fields on a V4 run.

Each key of args_schema describes one argument with type, an optional format and an optional required. Truto reads args_schema only for the ID formats in the table below. For those, a missing required argument refuses the run with 400, and so does a value that doesn't match its format:

format The value must be
integrated_account_id An integrated account in the job's environment
webhook_id A webhook in the job's environment
super_query A SuperQuery region: apac or wnam
datastore_id A datastore in the job's environment

For any other argument, type, format and required are not checked: a run without start_date below still starts, and {{args.start_date}} stays in the output as literal text. To refuse such a run, check the argument in args_validation, as the example does for start_date.

If every account named by an integrated_account_id argument needs reauthentication, the run fails as soon as it starts. See Sync Jobs overview.

"args_schema": {
  "integrated_account_id": { "type": "string", "format": "integrated_account_id", "required": true },
  "webhook_id": { "type": "string", "format": "webhook_id" },
  "start_date": { "type": "string", "format": "date", "required": true }
},
"args_validation": "$exists(args.start_date) ? null : { \"message\": \"start_date is required\" }"

Nodes

Every node has a name and a type. Names must be unique: depends_on and resources_to_persist refer to nodes by name. Every node also takes:

  • depends_on: the name of one parent node. A node without one starts the run.
  • run_if: a JSONata expression. The node runs only when it's truthy.
  • debug: true logs a transform's input and output, which helps when an expression misbehaves.

request

Calls an API for an integrated account.

Field What it does
resource <unified model>/<resource>, such as ticketing/tickets, for a Unified API call. A name without a /, such as tickets, is a Proxy API resource.
method list, get, or another method of the resource.
integrated_account_id Required. Usually {{args.integrated_account_id}}.
id The record ID for methods that take one.
query, body An object with {{placeholders}}, or a JSONata string that returns the object.
loop_on A dotted path, such as resources.ticketing.tickets. The node runs once per item, and each run sees that path set to one item.
recurse { "if": ..., "config": { "id": ..., "query": ... } }: call the same resource again for each record where if holds, such as child pages.
paginate_if A JSONata expression. Truto fetches the next page only while it's truthy. It sees payload and pagination (previous_total_records_size, current_page_records_size, total_records_size, next_cursor).
delay_after_ms A pause after each call. The longest of this and the job's and run's delays wins.

Later nodes read a request's records under resources, with each / in resource as a nested key: ticketing/tickets is resources.ticketing.tickets. In JSONata, quote a key with a hyphen in backticks, such as resources.`user-directory`.users.

transform

Runs config.expression, a JSONata expression over the parent's output, and passes the result on as payload.records. An empty result passes nothing. Set use_args: true when the parent isn't a request, such as a transform that builds records from args.

spool

Waits for every page and iteration of its parent, then passes all of it on at once. Put one before a transform that needs the whole set, such as one that combines pages into a single document.

add_context

Runs config.expression and merges the object it returns into the context of later nodes. Use it to expose a value, such as a watermark, as {{last_synced_at}} or last_synced_at in JSONata.

get_state, update_state, delete_state

Read and write values kept under the job's state_key, so one run can pick up where the last one stopped.

Node config
get_state key, and default, which is stored and used the first time the key has no value. Later nodes read it at resources.<node name>.value.
update_state key, and value (a placeholder template) or value_expression (JSONata).
delete_state key.

You can inspect and reset a job's state on its Run State tab in the dashboard.

event

config.name is start or complete. Nodes that depend on a start event run when the run starts. Nodes that depend on a complete event run after everything else, which is where an update_state that moves the watermark belongs.

destination

Sends the output of the nodes named in resources_to_persist somewhere. config.id names the target, usually from args. A destination's run_if also sees payload, the records about to be sent, so $count(payload.records) > 0 skips empty batches.

destination_type What it does
webhook Sends each batch to the webhook in config.id as a sync_job_run:record event. The run's sync_job_run:started, sync_job_run:completed and sync_job_run:failed events go to the same webhooks. A job with no webhook destination sends those to every webhook in the environment that subscribes to them.
datastore Calls method on the datastore in config.id, with config.config as the method's input. loop_on calls it once per item at that path. See the methods each type takes.

Placeholders and context

{{...}} placeholders and JSONata expressions read the same context:

  • args: the run's arguments.
  • resources: output of earlier requests and get_state nodes.
  • payload: in a destination, the batch being sent, so {{payload.records.0.file_name}} is the first record's file_name.
  • sync_job_run: the run, such as sync_job_run.started_at.toISOString() or sync_job_run.status.
  • Anything an add_context node added.

Add a type suffix when a value must not become text: {{resources.knowledge-base.page-content.has_children:bool}} is a boolean, and {{payload.records.0.parquet:any}} keeps the value as it is, such as binary Parquet built with $jsonToParquet. Expressions can use the $ functions of @truto/truto-jsonata.

An incremental job

This job reads tickets created since the last successful run and sends them to a webhook. The first run starts from args.start_date.

{
  "label": "Fetch tickets with incremental sync",
  "default_runtime_version": 4,
  "integration_name": "",
  "state_key": "{{args.integrated_account_id}}",
  "mutex_key": "{{args.integrated_account_id}}",
  "args_schema": {
    "start_date": { "type": "string", "format": "date", "required": true },
    "integrated_account_id": { "type": "string", "format": "integrated_account_id", "required": true },
    "webhook_id": { "type": "string", "format": "webhook_id", "required": true }
  },
  "args_validation": "$exists(args.start_date) ? null : { \"message\": \"start_date is required\" }",
  "resources": [
    {
      "type": "get_state",
      "name": "get-last-synced-at",
      "config": { "key": "last_synced_at", "default": "{{args.start_date}}" }
    },
    {
      "type": "add_context",
      "name": "add-last-synced-at",
      "depends_on": "get-last-synced-at",
      "config": { "expression": "{ 'last_synced_at': resources.`get-last-synced-at`.value }" }
    },
    {
      "type": "request",
      "name": "list-tickets",
      "resource": "ticketing/tickets",
      "method": "list",
      "depends_on": "get-last-synced-at",
      "query": { "created_at": { "gt": "{{last_synced_at}}" } },
      "integrated_account_id": "{{args.integrated_account_id}}"
    },
    {
      "name": "webhook",
      "type": "destination",
      "destination_type": "webhook",
      "config": { "id": "{{args.webhook_id}}" },
      "resources_to_persist": ["list-tickets"]
    },
    { "name": "on-complete", "type": "event", "config": { "name": "complete" } },
    {
      "type": "update_state",
      "name": "update-last-synced-at",
      "depends_on": "on-complete",
      "config": {
        "key": "last_synced_at",
        "value_expression": "sync_job_run.status = 'completed' ? sync_job_run.started_at.toISOString()"
      }
    }
  ]
}

Which filters a Unified API list accepts, such as created_at with gt, depends on the integration's mapping. Check the resource's query parameters in the Unified API reference before you rely on one.

Create the job, then start a run with its args:

truto sync-jobs create --stdin < job.json
truto sync-job-runs create -b '{
  "sync_job_id": "<sync_job_id>",
  "args": {
    "integrated_account_id": "<integrated_account_id>",
    "webhook_id": "<webhook_id>",
    "start_date": "2026-01-01"
  }
}'
curl -X POST "https://api.truto.one/sync-job" \
  -H "Authorization: Bearer <API token>" \
  -H "Content-Type: application/json" \
  -d @job.json
 
curl -X POST "https://api.truto.one/sync-job-run" \
  -H "Authorization: Bearer <API token>" \
  -H "Content-Type: application/json" \
  -d '{
    "sync_job_id": "<sync_job_id>",
    "args": {
      "integrated_account_id": "<integrated_account_id>",
      "webhook_id": "<webhook_id>",
      "start_date": "2026-01-01"
    }
  }'

How V4 differs from versions 1 to 3

Versions 1 to 3 Version 4
Node name and type Optional on requests. A node without type is a request. Required on every node.
Where output goes The run picks one destination: webhook_id, datastore_id, super_query or daemon_group_key (a daemon group). destination nodes in the job, with targets from args. A job can write to several.
What gets sent Requests marked persist: true, and transforms marked persist. The nodes listed in each destination's resources_to_persist.
Integrated account The run's integrated_account_id. Each request's integrated_account_id, usually from args. One run can read several accounts.
Datastores MongoDB Data API, one collection per resource (versions 2 and 3). Any datastore type with methods, called by name.
Incremental state Not available as nodes. get_state, update_state, delete_state and event nodes.
Response format response_format on a request: raw, unified or normalized. Not a node field.

A job runs on the version in its default_runtime_version.

Next

For the dashboard screens that create, run and inspect these jobs, see Using the Sync Jobs UI. To start a V4 run each time an account connects, use a workflow.