Built-in actions
Who this is for: people and AI assistants who write workflow steps with uses: platform/....
Before you start: read Steps in the workflow format.
Built-in actions are part of Liitora. Use them in any workflow as uses: platform/<name>@v1. They run inside Liitora, and they reach other systems only through Liitora’s safe HTTP client and your connections. The Inputs and Outputs tables below are made from the same JSON Schemas that Liitora uses to check your workflow when you save it and every step when it runs. You also find them in the app under Library, and your AI assistant reads them with the MCP tool get_action.
| Action | What it does | Connection slot |
|---|---|---|
http.request |
Calls an HTTP API. | api (optional) |
evaluate |
Computes a value, for example a check for if:. |
none |
transform |
Reshapes data with a small JavaScript function. | none |
json.map |
Picks and renames fields. | none |
csv.build |
Builds a CSV file from rows. | none |
csv.parse |
Reads CSV text or a CSV file into rows. | none |
pdf.render |
Renders an HTML template to a PDF file. | none |
zip |
Packs files into one zip file. | none |
unzip |
Unpacks a zip file into one file per entry. | none |
email.send |
Sends an email from your company’s sender. | none |
webhook.send |
Sends an outgoing webhook, optionally signed. | target (optional) |
state.get |
Reads a small saved value, such as a sync cursor. | none |
state.set |
Saves a small value, such as a sync cursor. | none |
sync.diff |
Finds created, updated and deleted records, against a target or the last successful run. | none |
xml.parse |
Reads XML text or an XML file into a value. | none |
xml.build |
Builds XML text or an XML file from a value. | none |
xml.split |
Reads the repeating records of an XML document as a list. | none |
xlsx.read |
Reads the rows of one sheet of an Excel workbook. | none |
xlsx.write |
Builds an Excel workbook from rows. | none |
fixed.parse |
Reads fixed-width records from text or a file. | none |
fixed.build |
Builds fixed-width records as text or a file. | none |
wait |
Pauses the run for a few seconds. | none |
ai.generate |
Asks your own AI model and checks the answer against a schema. | model |
files.list |
Lists files on SFTP or S3. | store |
files.download |
Downloads a file into an artifact. | store |
files.upload |
Writes a file. | store |
files.move |
Moves or renames a file. | store |
files.delete |
Deletes a file. | store |
How to read the tables
Section titled “How to read the tables”- Required says whether you must set the input. Some actions also need one input out of a group; the line under the table says so.
- Rules are checked when you save the workflow and again when the step runs. A value that breaks a rule fails the step with
invalid_input, and the message names the field without showing its value. - Output fields are what later steps read as
${{ steps.<id>.outputs.<field> }}. - Files an action makes are artifacts. Later steps use them as
${{ artifacts.<file name> }}.
http.request
Section titled “http.request”platform/http.request@v1 makes one HTTP call. Give url as a full https:// address, or as a path such as /v1/devices on the connection bound to the api slot. With a connection, Liitora adds the address and the sign-in for you.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
method |
string | no | one of GET, POST, PUT, PATCH, DELETE, HEAD; default "GET" |
HTTP method. |
url |
string | yes | not empty | Absolute https URL, or a path such as /v1/devices when the api connection is set. |
query |
object | no | none | Query parameters. |
headers |
object | no | none | Extra request headers. Authentication comes from the connection; never put secrets here. |
json |
any | no | none | Request body sent as JSON. |
body |
string | no | none | Raw request body. |
timeout_seconds |
integer | no | at least 1; at most 300; default 30 |
Request timeout. |
expect_status |
list of integer | no | each item: at least 100; at most 599 | Statuses treated as success; others fail the step. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
status |
integer | yes | none | Response status. |
headers |
object | yes | none | Response headers. |
json |
any | no | none | Response body parsed as JSON, when it is JSON. |
body |
string | no | none | Response body as text. |
body_encoding |
string | no | one of text, base64 |
base64 when body holds a binary response encoded as base64. |
Other fields are refused.
Rules:
- Send the body as
jsonor asbody, not both. - Without
expect_status, any status other than 2xx fails the step. With it, only the listed statuses succeed. A failure with 429 or a 5xx status can be retried withretry:. timeout_secondsis 1 to 300 seconds (default 30).- Addresses with a user name or password in them (
https://user:pass@...) are refused. - You can’t set the headers
Authorization,Proxy-Authorization,CookieorX-Api-Key. Sign-in comes from the connection. Transport headers such asHostandContent-Lengthare refused too. - Header values are one line. At most 50 extra headers, and the request body is at most 10 MB.
- Response headers have lower-case names, and repeated headers are joined with
,. A JSON response is injson, read with strict rules in the sandbox: whole numbers of 2^53 (9,007,199,254,740,992) or more come back as text so no digit is lost, and a body that is not valid JSON or is nested more than 64 levels deep comes back as text inbody. Other text is inbody. A binary response is inbodyas base64, withbody_encoding: base64. - Only HTTPS and public addresses work. Private and internal addresses, such as
10.0.0.1orlocalhost, are blocked. Redirects are not followed.
- id: fetch name: Fetch overdue devices from the ERP uses: platform/http.request@v1 connections: api: internal-erp with: method: GET url: /v1/devices query: status: overdueevaluate
Section titled “evaluate”platform/evaluate@v1 returns the value you give it. Use it for a named check that later steps use in if:, or to stop a run when something is wrong.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
expression |
any | yes | none | The value to compute, usually one ${{ }} expression. |
fail_if_false |
boolean | no | default false |
Fail the step when the value is false (false, 0, ‘’ or null). |
message |
string | no | not empty; at most 500 characters | Error message when fail_if_false fails the step. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
result |
any | yes | none | The computed value; if: treats false, 0, ‘’ and null as false. |
Other fields are refused.
With fail_if_false: true, a false value (false, 0, '' or null) fails the step with your message, or with “The check was false.”
- id: check name: Check whether any devices are overdue uses: platform/evaluate@v1 with: expression: ${{ steps.fetch.outputs.json.devices.length > 0 }}transform
Section titled “transform”platform/transform@v1 runs a small JavaScript function in the sandbox. script is the body of a function that gets input and returns the result. It has the same ctx as a run: script, see Host API for scripts.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
script |
string | yes | not empty | JavaScript function body run in the sandbox; receives input and returns the result. |
input |
any | no | none | Data passed to the script. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
result |
any | yes | none | What the script returned. |
Other fields are refused.
The returned value is in result. The script passes the same checks as any script when you save: no import, no eval, no passwords written into the code.
script can’t contain ${{ }} expressions. Data placed in the code could run as code, for example a webhook body that contains JavaScript. Pass data through input instead and read it in the script. A workflow with an expression in script can’t be saved (script_expression), and a step whose script has one fails without running.
with: input: { name: "${{ trigger.body.name }}" } script: return input.name.toUpperCase(); - id: totals name: Add up the invoice totals uses: platform/transform@v1 with: input: ${{ steps.shape.outputs.result }} script: | return input.reduce((sum, row) => sum + row.total, 0);json.map
Section titled “json.map”platform/json.map@v1 picks and renames fields of one object, or of each object in a list. The result has the same shape as data.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
data |
any | yes | none | The object or list of objects to map. |
mapping |
object | yes | none | Output field name to a JSONPath into each item, e.g. { id: "$.device.id" }. |
defaults |
object | no | none | Value per output field when its path finds nothing. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
result |
any | yes | none | The mapped object, or list of objects when data is a list. |
Other fields are refused.
Rules:
- A path is
$.a.b,$.items[0].id,$['key with spaces'],a.bor a JSON pointer such as/a/0/b. There are no wildcards or filters: one path finds at most one value. - A dotted output name, such as
customer.name, makes a nested object. - A path that finds nothing (or
null) gives the value fromdefaults, ornull. - Two outputs that overlap, such as
aanda.b, are refused. - At most 500 output fields, 100 000 items and a 32 MB result.
- id: shape name: Keep the fields the finance system needs uses: platform/json.map@v1 with: data: ${{ steps.fetch.outputs.items }} mapping: number: $.invoice_no customer.name: $.customer.name total: $.amount.total defaults: total: 0csv.build
Section titled “csv.build”platform/csv.build@v1 writes rows to a CSV file (text/csv; charset=utf-8).
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
rows |
list of object | yes | none | Rows to write; each row is an object. |
filename |
string | yes | not empty; at most 200 characters | File name of the artifact, e.g. overdue.csv. |
columns |
list of string | no | none | Columns in order. Defaults to the keys of the first row. |
delimiter |
string | no | not empty; one character; default "," |
Field separator. |
include_header |
boolean | no | default true |
Write a header row. |
bom |
boolean | no | default false |
Start the file with a UTF-8 byte order mark (helps Excel detect UTF-8). |
escape_formulas |
boolean | no | default true |
Prefix text cells that start with =, +, -, @, tab or carriage return (also after leading spaces, and the full-width signs such as =) with ' so spreadsheets do not run them as formulas. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
artifact |
string | yes | none | Name of the produced file artifact; later steps reference it as ${{ artifacts.<name> }}. |
rows |
integer | yes | at least 0 | Number of rows written. |
Other fields are refused.
Rules:
- Columns come in the order of
columns, or else the keys of the first row. - The file follows RFC 4180: lines end with CRLF, and cells with the delimiter, a quote or a line break are quoted.
- Text is written as it is. Numbers and true or false as in JSON.
nullis an empty cell. Lists and objects are compact JSON. - Spreadsheet formulas. With
escape_formulas(on by default), a text cell or header that starts with=,+,-,@, a tab or a carriage return (also after leading spaces, and the full-width signs such as=) gets a leading'. Excel, LibreOffice and Google Sheets then show it as text instead of running it. JSON numbers are never changed, so-5as a number stays a number. bom: truestarts the file with a UTF-8 byte order mark, which helps Excel read ä and ö correctly.- The delimiter is one character, not a quote or a line break.
- At most 100 000 rows, 1000 columns and 50 MB.
- id: report name: Build a CSV of the overdue devices uses: platform/csv.build@v1 with: rows: ${{ steps.fetch.outputs.json.devices }} columns: [id, owner, due_date] filename: overdue.csv bom: truecsv.parse
Section titled “csv.parse”platform/csv.parse@v1 reads CSV from text or from a file made earlier in the run.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
text |
string | no | none | CSV text to parse. |
artifact |
any | no | none | A CSV artifact, e.g. ${{ artifacts.import.csv }}. |
delimiter |
string | no | not empty; one character; default "," |
Field separator. |
has_header |
boolean | no | default true |
The first row names the columns. |
At least one of: text; artifact. Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
rows |
list of any | yes | none | Parsed rows (objects when has_header is true, lists otherwise). |
count |
integer | yes | at least 0 | Number of rows. |
Other fields are refused.
Rules:
- It reads CSV the same way as
ctx.formats.csv.parsein a script. See Data formats for the rules and limits all data formats share. - UTF-8 only. A byte order mark at the start is dropped. A byte that is not valid UTF-8 fails the step with
format_error. - With
has_header(the default), each row is an object keyed by the header. Empty header names becomecolumn_1and so on, and repeated names get_2,_3. - Every row must have as many fields as the first one. A shorter or longer row fails the step with
format_errorand its line number, so a missing field is never read as an empty one. - Without a header, each row is a list of texts.
- At most 50 MB, 100 000 rows and 1000 fields per row.
- A
format_erroris not retried, because the same file would fail the same way.
- id: read name: Read the imported price list uses: platform/csv.parse@v1 with: artifact: ${{ artifacts.prices.csv }} delimiter: ";"pdf.render
Section titled “pdf.render”platform/pdf.render@v1 renders an HTML template with data and lays it out as an A4 PDF file.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
template |
string | yes | not empty | Name of the HTML template to render. |
data |
object | no | none | Values available to the template. |
filename |
string | yes | not empty; at most 200 characters | File name of the artifact, e.g. report.pdf. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
artifact |
string | yes | none | Name of the produced file artifact; later steps reference it as ${{ artifacts.<name> }}. |
pages |
integer | yes | at least 1 | Number of pages. |
Other fields are refused.
template names a template stored in Settings > Templates. See Use email and PDF templates. A name that doesn’t exist fails the step with template_not_found.
What the layout supports:
- Headings (
h1toh6), paragraphs,div, line breaks, horizontal rules, bold and italic, and nested bullet and numbered lists. - Tables with equal-width columns, borders and a shaded header row. A cell taller than a page is cut.
- Page breaks where needed, and a page number at the bottom of each page. At most 500 pages.
- Not supported: CSS, images, links, colours, fonts and column widths.
scriptandstyleare removed with their content. - Text uses the standard PDF font Helvetica. Finnish, Swedish and other Western European text is fine. Characters outside Windows-1252, such as emoji, Cyrillic, Arabic or Chinese, print as
?.
platform/zip@v1 packs files made earlier in the run into one zip file.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
files |
list of any | yes | at least 1 item | Artifacts to put in the archive. |
filename |
string | yes | not empty; at most 200 characters | File name of the archive, e.g. reports.zip. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
artifact |
string | yes | none | Name of the produced file artifact; later steps reference it as ${{ artifacts.<name> }}. |
count |
integer | yes | at least 1 | Number of files in the archive. |
Other fields are refused.
Rules:
- Only the last part of each file name is kept, so an archive can never write outside the folder it is unpacked into.
\counts as a folder separator too. - Control characters and
:become_, leading dots are removed, and empty names becomefile. - Repeated names get
(2),(3)before the extension. - At most 100 files and 100 MB of input.
- id: bundle name: Pack the reports into one file uses: platform/zip@v1 with: files: ["${{ artifacts.overdue.csv }}", "${{ artifacts.summary.csv }}"] filename: reports.zipplatform/unzip@v1 unpacks a zip file made or downloaded earlier in the run. Every file in the archive becomes a file of the run, which later steps use as ${{ artifacts.<name> }}.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
artifact |
any | yes | none | The zip file artifact to unpack, e.g. ${{ artifacts['orders.zip'] }}. |
prefix |
string | no | at most 50 characters; letters, digits, ., _ and - |
Text put before every unpacked file name, e.g. in-. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
artifacts |
list of string | yes | none | Names of the unpacked files, in archive order. |
count |
integer | yes | at least 0 | Number of files unpacked. |
skipped |
integer | yes | at least 0 | Number of folders in the archive. Folders are not unpacked. |
Other fields are refused.
Rules:
- A file’s name is made from its path in the archive. Folder names are joined with
_, so2026/q1/report.csvbecomes2026_q1_report.csv..and..parts are dropped, and any character other than a letter, a digit,.,_or-becomes_. A file can never land outside the run. - If two files would get the same name, the step fails with
invalid_inputand nothing is unpacked. Useprefixto keep names apart from files the run already has. - The archive is read in a sealed sandbox, and strictly. These fail with
format_error, and nothing is unpacked:- a file that is not a zip archive, or is damaged (a file whose checksum does not match, extra data after the end of the archive),
- links, password protected or encrypted archives, archives split into parts, and ZIP64 archives (only needed for files of 4 GB or more),
- compression other than “stored” and “deflate” (every common zip tool uses these),
- two files whose names differ only in upper and lower case, such as
Report.csvandreport.csv.
- At most 100 files (and 1,000 files and folders), and 100 MB unpacked in total. A file larger than 1 MB may unpack to at most 100 times its packed size. These limits are checked before anything is unpacked, and a larger archive fails with
format_error, so a “zip bomb” can’t fill your storage. - A file keeps a type such as CSV, PDF or PNG only when its name and its first bytes agree. Everything else, web pages and SVG images included, is stored as a plain download.
- Each unpacked file counts toward the run’s file limits and your plan’s
storage_mb.
- id: unpack name: Unpack the supplier's order export uses: platform/unzip@v1 with: artifact: "${{ artifacts['orders.zip'] }}" prefix: in- - id: read name: Read the orders uses: platform/csv.parse@v1 with: artifact: "${{ artifacts['in-orders.csv'] }}"email.send
Section titled “email.send”platform/email.send@v1 sends an email from your company’s verified sender. See Set up email senders.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
to |
list of string | yes | at least 1 item; at most 50 items; each item: at least 3 characters | Recipients. |
cc |
list of string | no | at most 50 items; each item: at least 3 characters | Copy recipients. |
bcc |
list of string | no | at most 50 items; each item: at least 3 characters | Blind copy recipients. |
subject |
string | yes | not empty; at most 250 characters | Subject line. |
template |
string | no | none | Name of the tenant email template, filled with data. Every value is escaped for where it goes in the HTML. |
text |
string | no | none | Plain-text body. |
html |
string | no | none | Literal HTML body. It cannot contain ${{ }} expressions: to put values in HTML, use template and data. |
data |
object | no | none | Values available to the template. They are escaped, never read as HTML or template syntax. |
attachments |
list of any | no | at most 10 items | Artifacts to attach, e.g. ${{ artifacts.overdue.csv }}. |
At least one of: template; text; html. Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
message_id |
string | yes | none | Identifier of the sent message. |
accepted |
list of string | yes | none | Recipients the mail server accepted. |
Other fields are refused.
Rules:
- The body is
htmlortext, or both. With onlyhtml, Liitora makes the plain-text part from it. templatenames a template stored in Settings > Templates and fills it withdata. See Use email and PDF templates. Usetemplateorhtml, not both.htmlis fixed HTML that you write. It can’t contain${{ }}expressions, because data placed in it could add links, forms or hidden text to a message sent from your domain. A workflow with an expression inhtmlcan’t be saved (html_expression), and a step whosehtmlhas one fails withinvalid_inputand sends nothing. To put values in HTML, store the HTML as a template and give the values indata.subjectandtextare plain text and can use expressions.- Addresses are plain
name@example.com. Display names (Anna <anna@example.com>) and IP addresses are refused. Repeated addresses are sent once. - At most 50 addresses per list and 100 in total. The subject is one line of at most 250 characters.
- Text is at most 1 MB and HTML at most 2 MB. At most 10 attachments, 20 MB together.
acceptedlists the addresses the mail server accepted.- If your company has email senders, the default one (or else the first verified one) sends. An unverified sender fails the step with
email_sender_unverified. Without any sender of your own, Liitora’s own sender address is used.
- id: email name: Email the report to IT uses: platform/email.send@v1 with: to: [it@example.com] subject: "Overdue devices: ${{ steps.report.outputs.rows }}" text: The list is attached. attachments: ["${{ artifacts.overdue.csv }}"]webhook.send
Section titled “webhook.send”platform/webhook.send@v1 sends JSON with POST to an https:// address, or to a path on the connection bound to the target slot.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
url |
string | yes | not empty | Absolute https URL, or a path when the target connection is set. |
json |
any | no | none | Body sent as JSON. |
headers |
object | no | none | Extra headers. |
sign |
boolean | no | default false |
Add an HMAC signature header using the connection’s signing secret. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
status |
integer | yes | none | Response status. |
body |
string | no | none | Response body as text. |
Other fields are refused.
Rules:
- Every request has an
Idempotency-Keyheader. It stays the same when the step is retried, so the receiver can drop a repeat. - With
sign: true, Liitora addsX-Liitora-Timestamp(unix seconds) andX-Liitora-Signature: sha256=<hex>: the HMAC-SHA256 of<timestamp>.<body>with the signing secret of thetargetconnection. The action never sees the secret. Give the connection a secret reference namedsigning_secret. Without it the step fails withsigning_unavailable. - You can’t set
Idempotency-Keyor anyX-Liitora-header yourself. - Any status other than 2xx fails the step. 429 and 5xx can be retried. The response body is kept up to 64 KB. The timeout is 30 seconds.
The receiver checks the signature the same way as for incoming webhooks, see Webhook signing.
- id: notify name: Tell the IT channel about the new report uses: platform/webhook.send@v1 connections: target: it-chat with: url: /hooks/it sign: true json: text: "The overdue devices report is ready."state.get
Section titled “state.get”platform/state.get@v1 reads a small value saved by an earlier run of the same workflow, such as the time of the last sync.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
key |
string | yes | not empty; at most 200 characters | State key, e.g. last_synced_at. |
default |
any | no | none | Returned when the key is not set. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
value |
any | yes | none | The stored value, or default. |
found |
boolean | yes | none | Whether the key was set. |
Other fields are refused.
- id: cursor name: Read when the last sync ran uses: platform/state.get@v1 with: key: last_synced_at default: "2026-01-01T00:00:00Z"state.set
Section titled “state.set”platform/state.set@v1 saves a small value for later runs of the same workflow.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
key |
string | yes | not empty; at most 200 characters | State key. |
value |
any | yes | none | Value to store (small JSON). |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
previous |
any | yes | none | The value before this step, or null. |
Other fields are refused.
Rules:
- A value is at most 64 KB of JSON. Values belong to one workflow in your company.
state.setreads the old value first and then writes. Two runs at the same time can both see the sameprevious. If the value must never be read twice, addconcurrency:to the workflow.- Test cases can set the state before the run with
given.state.
- id: remember name: Remember when this sync ran uses: platform/state.set@v1 with: key: last_synced_at value: ${{ trigger.now }}sync.diff
Section titled “sync.diff”platform/sync.diff@v1 compares records by key and tells you which ones were created, updated or deleted. Compare the source with a target list, such as what the receiving system has now. Or, for a source that cannot filter by “modified since”, set snapshot: true to compare it with what the same step saw in the last successful run.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
source |
list of objects | yes | at most 50,000 records and 20 MB | The records as they are now. |
target |
list of objects | one of two | at most 50,000 records and 20 MB | The records to compare with, e.g. what the receiving system has. Leave out with snapshot: true. |
snapshot |
boolean | one of two | default false |
Compare source with the source of the last successful run instead of a target. |
key |
string or list of string | yes | 1 to 10 fields, no repeats; each name at most 200 characters | The field that identifies a record, e.g. id, or a list of fields for a composite key. |
compare |
list of string | no | at most 500 fields, no repeats | Compare only these fields. Default: every field. |
ignore |
list of string | no | at most 500 fields, no repeats | Fields never compared, e.g. updated_at. |
trim_strings |
boolean | no | default false |
Ignore spaces at the start and end of text values and keys. |
allow_empty_source |
boolean | no | default false |
Accept an empty source that deletes every record. Default: an empty source fails the step. |
Set target or snapshot: true, not both. Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
mode |
string | yes | one of target, snapshot |
What the source was compared with. |
counts |
object | yes | none | source, target, created, updated, deleted and unchanged, each a number. In snapshot mode target counts the records in the snapshot used. |
created |
list of objects | yes | none | Source records whose key is new, in source order. Each has key (the key fields and their values) and record (the source record). |
updated |
list of objects | yes | none | Source records whose compared fields changed, in source order, with key and record. In target mode each also has changes: a list of {field, from, to}, where from is the target value and to the source value. |
deleted |
list of objects | yes | none | Keys no longer in the source, with key. In target mode each also has record, the target record. |
snapshot |
object or null | yes | none | Snapshot mode only, else null. previous is none (no snapshot yet), used or reset (made with other settings, so every record counts as created). save_on_success says whether this run saves a new snapshot when it succeeds. |
Other fields are refused.
Unchanged records are only counted, not listed, so the output stays small.
Rules:
- Keys. Key values are text, numbers or true/false. A key field that is missing or null fails the step, and so does a key that is an object or a list. Keys are strict about type:
1and"1"are different records. A key is at most 1 KB, and two records on the same side with the same key fail the step. - Comparing values. Key fields never count as changes. Numbers compare by value, so
1.0equals1, but1and"1"differ. A missing field is the same asnull. Nested objects and lists compare as a whole value: a change anywhere inside is one change of that field. Lists compare in order. Text compares exactly, so letters with accents written in two different ways in Unicode count as different. - Empty source. When the source is empty but the target or snapshot has records, the step fails with
sync_source_empty. A source system that has an outage and returns nothing would otherwise look like “delete everything”. Setallow_empty_source: truewhen an empty source is real. - Snapshot mode.
- The snapshot is saved only when the whole run succeeds. If a later step fails, the next run reports the same changes again, so none are lost.
- Dry runs and test runs read the snapshot but never save it, and they show
save_on_success: false. A test case starts without a snapshot unless it gives one ingiven.snapshots. - Each workflow step has its own snapshot, separately for test and production. If you change
key,compare,ignoreortrim_strings, the next run starts over (previous: reset). - Two runs at the same time: the first to succeed saves its snapshot, and the other one’s changes are reported again next time.
- Archiving the workflow deletes its snapshots. You can see each snapshot’s record count and when it was saved on the workflow’s Overview tab, and reset one there so the next run starts over. See Reset what a sync step remembers.
- Snapshot mode cannot be used inside
for_each. - On your own runner, snapshot mode needs a runner group that sends everything back. See Change detection on a runner.
- What a snapshot keeps. Liitora keeps the record keys and a fingerprint (a salted hash) of each record’s compared fields, never the field values. That is why snapshot mode lists no
changesand gives deleted records by key only. The keys are stored as they are, so use an id rather than personal data such as an email address as the key where you can. A key that contains a secret value fails the step withsync_key_secret. - Errors. The step fails with one of these codes. The messages name the record’s position, such as
source[3], and the field, never its value.sync_record_invalid: a record is not an object.sync_key_missing,sync_key_invalid,sync_key_too_longandsync_duplicate_key: see Keys above.sync_too_many_recordsorsync_input_too_large: a side has over 50,000 records or 20 MB.sync_snapshot_too_large: the snapshot would be over 16 MB.sync_target_missingorsync_target_with_snapshot: set eithertargetorsnapshot: true.sync_snapshot_in_for_each: snapshot mode was used insidefor_each.sync_snapshot_unavailable: the snapshot could not be read or saved. The step can be retried.
- id: diff name: Find contacts that changed since the last sync uses: platform/sync.diff@v1 with: source: ${{ steps.fetch.outputs.json.contacts }} snapshot: true key: id ignore: [updated_at] trim_strings: trueCompare with what the receiving system has, with a composite key:
- id: diff name: Compare prices with the shop uses: platform/sync.diff@v1 with: source: ${{ steps.erp.outputs.json.prices }} target: ${{ steps.shop.outputs.json.prices }} key: [sku, currency] compare: [amount, valid_from]xml.parse
Section titled “xml.parse”platform/xml.parse@v1 reads XML into a value that later steps use like JSON. It reads XML the same way as ctx.formats.xml.parse in a script, see XML in scripts. Every option and limit is listed in Data formats.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
text |
string | one of two | at most 8 MB | XML text to read. |
artifact |
any | one of two | at most 8 MB | An XML file, e.g. ${{ artifacts.import.xml }}. |
charset |
string | no | one of utf-8, utf-16, windows-1252, iso-8859-1, iso-8859-15, us-ascii (and their usual aliases); default utf-8 |
The character set of the file. If the XML declaration names one, it must be the same. |
arrays |
"always" or list of string |
no | none | Element paths such as order.items.item that are always lists, even with one element. "always" makes every child element a list. |
namespaces |
string | no | one of strip, prefix, uri; default strip |
strip keeps local names, prefix keeps names as written, uri names namespaced elements {uri}name. |
Set text or artifact, not both. Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
value |
object | yes | none | The document: an object with one key, the root element. |
Other fields are refused.
How XML becomes a value:
- An element with only text is a string:
<name>Ada</name>becomes"name": "Ada". An empty element is"". - Attributes are keys that start with
@, and the text next to attributes or child elements is#text:<price currency="EUR">9.50</price>becomes"price": {"@currency": "EUR", "#text": "9.50"}. - An element that appears more than once is a list. Use
arraysfor elements that can appear once or many times, so the shape does not change with the data. - All values are text. Convert numbers and dates in a later step.
- Comments and processing instructions are left out. CDATA is text.
For safety, a document with a DOCTYPE is refused, and so are entities other than the five XML ones (&, <, >, ", '). Documents are limited to 8 MB, 64 levels of nesting and 200,000 elements, attributes and text pieces. A file that is not valid in its character set fails the step. These failures are format_error, and the step is not retried, because the same XML fails the same way again.
- id: order name: Read the order uses: platform/xml.parse@v1 with: artifact: ${{ artifacts.order.xml }} arrays: [order.lines.line]xml.build
Section titled “xml.build”platform/xml.build@v1 writes a value as XML, for a system that takes XML. The value has the same shape xml.parse gives.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
value |
object | yes | one key, the root element | The document to write. |
declaration |
boolean | no | default true |
Start with <?xml version="1.0" encoding="UTF-8"?>. |
filename |
string | no | 1 to 255 characters, a file name without folders | Write the XML to a file with this name instead of returning it as text. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
xml |
string | no | none | The XML text, when filename is not set. |
artifact |
string | no | none | The name of the file, when filename is set. Later steps use it as ${{ artifacts.<name> }}. |
bytes |
integer | yes | at least 0 | The size of the XML in bytes. |
Other fields are refused.
Rules:
- Text is escaped for you, so
<,&and quotes in values are safe. - Numbers and true/false are written as text. A
nullvalue writes an empty element. - Element and attribute names must be valid XML names, and a prefix such as
p:needs its@xmlns:pdeclaration. - Workflow values do not keep the order of keys in an object. When the order of child elements matters, list them in
#content, which is a list:{"order": {"#content": [{"id": "1"}, {"date": "2026-10-04"}]}}. - The XML is at most 16 MB.
- id: invoice name: Write the invoice XML uses: platform/xml.build@v1 with: value: invoice: "@version": "2" number: ${{ steps.fetch.outputs.json.number }} total: ${{ steps.fetch.outputs.json.total }} filename: invoice.xmlxml.split
Section titled “xml.split”platform/xml.split@v1 reads the repeating records of an XML document, such as every <order> in <orders>, as a list. Use it with for_each to handle each record in its own step.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
path |
string | yes | 1 to 1024 characters | Element names from the root to the record, joined by dots, e.g. orders.order. |
text |
string | one of two | at most 8 MB | XML text to read. |
artifact |
any | one of two | at most 8 MB | An XML file, e.g. ${{ artifacts.import.xml }}. |
charset |
string | no | one of utf-8, utf-16, windows-1252, iso-8859-1, iso-8859-15, us-ascii (and their usual aliases); default utf-8 |
The character set of the file. If the XML declaration names one, it must be the same. |
arrays |
"always" or list of string |
no | none | Element paths such as order.items.item that are always lists, even with one element. "always" makes every child element a list. |
namespaces |
string | no | one of strip, prefix, uri; default strip |
strip keeps local names, prefix keeps names as written, uri names namespaced elements {uri}name. |
Set text or artifact, not both. Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
records |
list | yes | none | The elements at path, in document order, each in the shape xml.parse gives it. |
count |
integer | yes | at least 0 | The number of records. |
Other fields are refused.
Rules:
- One record is still a list with one item.
- When the element before the last one exists but has no records, the list is empty. When any other part of the path is missing, the step fails, so a typing mistake in
pathdoes not look like “no records”. - When an element on the path repeats, the records of each one are listed in order:
r.group.itemlists the items of every group. - At most 100,000 records. The limits of
xml.parseapply too.
- id: orders name: Split the export into orders uses: platform/xml.split@v1 with: artifact: ${{ artifacts.export.xml }} path: orders.order arrays: [orders.order.line]xlsx.read
Section titled “xlsx.read”platform/xlsx.read@v1 reads the rows of one sheet of an Excel workbook (.xlsx) made earlier in the run, for example one downloaded with files.download or received by email.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
artifact |
any | yes | at most 16 MB | An .xlsx file, e.g. ${{ artifacts.report.xlsx }}. |
sheet |
string or integer | no | at least 0; default the first sheet | The sheet to read: its name (an exact match first, then ignoring case) or its number, counting from 0. |
has_header |
boolean | no | default true |
Row 1 names the columns and every later row becomes an object. Otherwise every row is a list. |
dates |
string | no | one of iso, serial; default "iso" |
Cells with a date or time format become ISO 8601 text (iso) or stay Excel serial numbers (serial). |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
rows |
list | yes | none | The rows (objects when has_header is true, lists otherwise). Empty cells are null. |
count |
integer | yes | at least 0 | Number of rows. |
sheet |
string | yes | none | Name of the sheet that was read. |
sheets |
list of string | yes | none | Names of all sheets in the workbook, in order. |
epoch |
integer | yes | one of 1900, 1904 |
The workbook’s date system. |
formulas |
integer | yes | at least 0 | How many formula cells were read. Their values are the ones Excel saved, never recalculated. |
Other fields are refused.
Rules:
- It reads workbooks the same way as
ctx.formats.xlsx.readin a script. See Excel workbooks for the rules all workbooks follow. - Only
.xlsx. Macro workbooks (.xlsm), binary workbooks (.xlsb) and old.xlsfiles fail the step withformat_error, so a file that could carry code is never opened. - Formulas are never run. A formula cell gives the value Excel saved with it.
formulastells you how many there were, so you can spot a file whose values may be out of date. - Numbers come back as numbers, true and false as booleans, and text as text. An error cell such as
#N/Acomes back as its text. - Date cells become ISO 8601 text:
2026-10-04for a date,2026-10-04T08:30:00for a date and time,08:30:00for a time. Both the 1900 and the 1904 date systems are read correctly. - With
has_header(the default), empty header names becomecolumn_1and so on, and repeated names get_2,_3. - The rows start at cell A1, so empty leading rows and columns are kept as empty cells.
- At most 16 MB, 100,000 rows and 1,000 columns. Zip limits also apply: see
xlsx.readlimits. A workbook that unpacks to far more than its size, or holds another archive, fails the step. - A
format_erroris not retried, because the same file would fail the same way.
- id: read name: Read the price list uses: platform/xlsx.read@v1 with: artifact: ${{ artifacts.prices.xlsx }} sheet: Pricesxlsx.write
Section titled “xlsx.write”platform/xlsx.write@v1 writes rows to an Excel workbook (.xlsx) with one sheet.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
rows |
list of object | yes | none | Rows to write; each row is an object. |
filename |
string | yes | not empty; at most 200 characters | File name of the artifact, e.g. report.xlsx. |
columns |
list of string | no | none | Columns in order. Defaults to the keys of the first row. |
sheet |
string | no | 1 to 31 characters; default "Sheet1" |
Name of the sheet. |
include_header |
boolean | no | default true |
Write a header row. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
artifact |
string | yes | none | Name of the produced file artifact; later steps reference it as ${{ artifacts.<name> }}. |
rows |
integer | yes | at least 0 | Number of rows written, not counting the header. |
Other fields are refused.
Rules:
- Columns come in the order of
columns, or else the keys of the first row. A missing value is an empty cell, and keys not incolumnsare left out. - Numbers are written as numbers, true and false as booleans, and
nullas an empty cell. Lists and objects are written as compact JSON text. - Spreadsheet formulas. A text cell or header that starts with
=,+,-,@, a tab or a carriage return (also after leading spaces, and the full-width signs such as=) always gets a leading', so Excel shows it as text instead of running it. Numbers are never changed. - A sheet name can’t contain
\ / ? * [ ] :and can’t start or end with'. - A cell holds at most 32,767 characters. A longer value fails the step with
format_errorinstead of being cut. - At most 100,000 rows and 1,000 columns.
- id: report name: Build the monthly report uses: platform/xlsx.write@v1 with: rows: ${{ steps.fetch.outputs.json.devices }} columns: [id, owner, due_date] sheet: Overdue filename: overdue.xlsxfixed.parse
Section titled “fixed.parse”platform/fixed.parse@v1 reads fixed-width records, where every field has a set position and width on each line, as many bank, payroll and older systems use.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
fields |
list of objects | yes | at least one; see below | The layout. Overlapping fields are refused. |
unit |
string | yes | one of chars, bytes |
What start and width count: characters of the text, or bytes in encoding. |
text |
string | one of two | at most 16 MB | Fixed-width text to parse. |
artifact |
any | one of two | at most 16 MB | A fixed-width file, e.g. ${{ artifacts.payments.txt }}. |
encoding |
string | no | one of utf-8, utf-16, windows-1252, iso-8859-1, iso-8859-15, us-ascii (and their usual aliases); default utf-8 |
The character set of the file, and of the bytes that unit: bytes counts. utf-16 works with unit: chars only. |
trim |
string | no | one of both, left, right, none; default "both" |
Which padding spaces to remove from every field. |
short_lines |
string | no | one of error, pad; default "error" |
A line shorter than the record fails the step, or is read as if padded with spaces. |
long_lines |
string | no | one of error, ignore; default "error" |
A line longer than the record fails the step, or its extra characters are dropped. |
Each field has a name, a width of at least 1 and an optional start, counting from 1, which defaults to right after the previous field. A field can set its own trim. Set text or artifact, not both. Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
rows |
list of object | yes | none | One object per record, with each field’s value as text. |
count |
integer | yes | at least 0 | Number of records. |
Other fields are refused.
Rules:
- It reads records the same way as
ctx.formats.fixed.parsein a script. See Fixed-width records. - Every line is one record. Lines end with LF or CRLF, and empty lines are skipped.
- Use
unit: byteswhen the file’s specification counts bytes and the file has letters such as ä or ö that take more than one byte in its encoding. A field that would split a character fails the step. - The layout is checked before any data is read: overlapping fields, repeated names and fields past 65,536 characters fail the step with
invalid_input. - Gaps between fields are allowed and are not read.
- Values are always text. Use
json.maporevaluateto turn them into numbers or dates. - At most 16 MB, 100,000 records and 1,000 fields.
- id: payments name: Read the payment file uses: platform/fixed.parse@v1 with: artifact: ${{ artifacts.payments.txt }} unit: bytes encoding: iso-8859-1 fields: - { name: account, width: 18 } - { name: amount, width: 12 } - { name: reference, start: 32, width: 20 }fixed.build
Section titled “fixed.build”platform/fixed.build@v1 writes rows as fixed-width records, as text or as a file.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
rows |
list of object | yes | none | Rows to write; each row is an object. A missing or null value is all padding. |
fields |
list of objects | yes | at least one; see below | The layout. Overlapping fields are refused; gaps are filled with spaces. |
unit |
string | yes | one of chars, bytes |
What start and width count: characters, or bytes in encoding. |
encoding |
string | no | one of utf-8, windows-1252, iso-8859-1, iso-8859-15, us-ascii (and their usual aliases); unit: bytes only |
The character set of the file. Anything other than UTF-8 needs filename. |
line_ending |
string | no | one of "\n", "\r\n"; default "\n" |
Written after every record. |
filename |
string | no | not empty; at most 200 characters | Write the records to a file with this name instead of returning them as text. |
Each field has a name, a width of at least 1, an optional start (counting from 1, defaults to right after the previous field), an optional align (left, the default, or right) and an optional pad, one character (a space by default). Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
text |
string | no | none | The records, when filename is not set. |
artifact |
string | no | none | Name of the produced file artifact, when filename is set; later steps reference it as ${{ artifacts.<name> }}. |
rows |
integer | yes | at least 0 | Number of records written. |
Other fields are refused.
Rules:
- Text is written as it is. Numbers and true or false as in JSON. Lists and objects fail the step.
- Nothing is cut. A value longer than its field fails the step with
format_error, and so does a character the encoding can’t write. Make values fit first, for example withjson.map. - Right-aligned numbers with
pad: "0"give zero-filled amounts such as000125. - A file has the content type
text/plainwith the encoding as its charset. - At most 100,000 records and 16 MB.
- id: export name: Write the payroll file uses: platform/fixed.build@v1 with: rows: ${{ steps.fetch.outputs.json.payments }} unit: bytes encoding: iso-8859-1 line_ending: "\r\n" fields: - { name: employee, width: 10 } - { name: amount, width: 12, align: right, pad: "0" } filename: payroll.txtplatform/wait@v1 pauses the run for a few seconds, for example to give another system time to finish.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
seconds |
integer | yes | at least 1; at most 30 | How long to wait. Short delays only in the MVP. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
waited_seconds |
integer | yes | at least 0 | How long the step waited. |
Other fields are refused.
Longer waits are not available yet.
- id: pause name: Give the ERP time to finish the export uses: platform/wait@v1 with: seconds: 10ai.generate
Section titled “ai.generate”platform/ai.generate@v1 asks your own AI model to classify, summarise or extract, and checks the answer against a JSON Schema. The model slot takes an AI connection, see Connect an AI model.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
prompt |
string | yes | 1 to 32768 characters | What the model should do, for example “Classify this support ticket”. Sent as the system instruction. |
prompt_version |
string | no | at most 100 characters | Your own label for this prompt, for example ticket-triage-3. It is logged with every call. |
input |
any | no | none | The data the model works on, sent as JSON. Send only the fields the task needs. |
output_schema |
object | yes | type: object at the root; at most 64 KB |
JSON Schema the answer must match. |
model |
string | yes | 1 to 200 characters | The model to use, for example gpt-4o-mini or mistral-small-latest. On Azure OpenAI this is your deployment name. |
max_output_tokens |
integer | no | 1 to 32768; default 1024 |
Most tokens the answer may use. |
timeout_seconds |
integer | no | 1 to 300; default 120 |
How long one call may take. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
output |
any | yes | none | The answer, checked against output_schema. Null only when the call was not sent (a dry run). |
provider |
string | yes | openai, azure_openai, anthropic, mistral or openai_compatible |
The AI provider that answered. |
model |
string | yes | none | The model that was asked. |
prompt_version |
string | no | none | The prompt_version you gave. |
stop_reason |
string | no | none | Why the model stopped, as the provider reported it. |
request_id |
string | no | none | The provider’s ID for the call, for support requests. |
usage |
object | yes | input_tokens, output_tokens, total_tokens |
Tokens the call used. |
not_sent |
boolean | no | none | True when the call was not sent, because this is a dry run. |
Other fields are refused.
Rules:
- The step fails when the answer is refused (
ai_refused), cut off atmax_output_tokens(ai_truncated), removed by a content filter (ai_content_filter), or isn’t JSON that matchesoutput_schema(ai_invalid_output). The answer is then not used. See AI errors. - A busy provider or a rate limit is tried again, up to 3 calls in all, waiting as long as the provider asks (at most 60 seconds).
- The provider’s reply and the answer are read with strict JSON rules in the sandbox, like a JSON answer of
http.request. A whole number of 2^53 (9,007,199,254,740,992) or more in the answer comes back as text, so it doesn’t matchtype: integer; ask for such IDs as strings. The reading counts toward compute units. - Each call counts against the run’s AI token limit (
ai_tokens_per_run, see Limits per plan). - The request with the prompt, the input and the schema is at most 1 MB.
- Liitora never sends who the end user is, and the HTTP log keeps no request or response bodies of AI calls.
- id: triage name: Label the support ticket uses: platform/ai.generate@v1 connections: model: company-ai with: model: mistral-small-latest prompt: Label the ticket as bug, question or billing. input: subject: ${{ trigger.body.subject }} output_schema: type: object properties: label: { type: string, enum: [bug, question, billing] } required: [label] additionalProperties: falseFile actions
Section titled “File actions”The files.* actions read and write files on an SFTP server or an S3 bucket. Each one needs the store connection slot, filled with a file connection. Every path is relative to the connection’s base folder (SFTP) or prefix (S3). . and .. are not allowed, also not written with percent signs (such as %2e%2e), so a path can never leave the base. Other percent signs are part of the name: a%2Fb is one file called a%2Fb. On SFTP, a path that goes through a symbolic link is refused, so use the folder’s real path. In a dry run, files.upload, files.move and files.delete change nothing and only report what they would have done.
files.list
Section titled “files.list”platform/files.list@v1 lists the regular files in a folder, sorted by path.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
path |
string | no | at most 1024 characters; default empty | The folder. Empty lists the base itself. |
pattern |
string | no | 1 to 200 characters | Only names that match, with *, ? and [abc], for example *.csv. |
modified_since |
string | no | RFC 3339 time | Only files modified at or after this time. |
limit |
integer | no | 1 to 1000; default 100 | At most this many files. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
files |
array | yes | Each file with path, name, size and modified. |
|
count |
integer | yes | at least 0 | How many files were listed. |
- id: list uses: platform/files.list@v1 connections: store: partner-sftp with: path: outbox pattern: "*.csv"files.download
Section titled “files.download”platform/files.download@v1 copies a file into an artifact that later steps can read.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
path |
string | yes | 1 to 1024 characters | The file, for example ${{ trigger.file.path }}. |
artifact_name |
string | no | 1 to 200 characters | Name of the artifact. The default is the file name. |
content_type |
string | no | 1 to 200 characters | Media type of the artifact. The default is guessed from the file name. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
artifact |
object | yes | The stored artifact with id, name, content_type and size_bytes. |
|
file |
object | yes | The file with path, name, size and modified. |
- id: download uses: platform/files.download@v1 connections: store: partner-sftp with: path: ${{ trigger.file.path }}files.upload
Section titled “files.upload”platform/files.upload@v1 writes an artifact, text or JSON to a file.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
path |
string | yes | 1 to 1024 characters | The target file. On SFTP its folder must exist. |
artifact |
artifact | one of three | An artifact to upload. | |
text |
string | one of three | Text to upload (UTF-8). | |
json |
any | one of three | A value to upload as JSON. | |
overwrite |
string | no | fail, replace or skip; default fail |
What to do when the target exists. |
Set exactly one of artifact, text and json. Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
path |
string | yes | The target path. | |
size |
integer | yes | at least 0 | Bytes written, or that would have been written. |
written |
boolean | yes | False when the target existed and overwrite was skip, or in a dry run. |
- id: upload uses: platform/files.upload@v1 connections: store: partner-sftp with: path: inbox/report.csv artifact: ${{ artifacts.report.csv }} overwrite: replacefiles.move
Section titled “files.move”platform/files.move@v1 moves or renames a file, for example into an archive folder.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
from |
string | yes | 1 to 1024 characters | The file to move. |
to |
string | yes | 1 to 1024 characters | The new path. A path that ends with / keeps the file name. |
overwrite |
string | no | fail, replace or skip; default fail |
What to do when the target exists. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
from |
string | yes | The old path. | |
to |
string | yes | The resolved new path. | |
moved |
boolean | yes | False when the target existed and overwrite was skip, or in a dry run. |
- id: archive uses: platform/files.move@v1 connections: store: partner-sftp with: from: ${{ trigger.file.path }} to: archive/files.delete
Section titled “files.delete”platform/files.delete@v1 deletes a file.
Inputs
Section titled “Inputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
path |
string | yes | 1 to 1024 characters | The file to delete. |
missing_ok |
boolean | no | default false | Succeed when the file does not exist. By default the step fails. |
Other fields are refused.
Outputs
Section titled “Outputs”| Field | Type | Required | Rules | What it is |
|---|---|---|---|---|
path |
string | yes | The path. | |
deleted |
boolean | yes | False when the file did not exist, or in a dry run. |
- id: cleanup uses: platform/files.delete@v1 connections: store: partner-sftp with: path: outbox/old.csv missing_ok: trueTemplates for email and PDF
Section titled “Templates for email and PDF”email.send (with template) and pdf.render use one small template language without logic. You store templates in Settings > Templates, or your AI assistant stores them with set_template. See Use email and PDF templates.
{{ path }}inserts a value, such as{{ customer.name }},{{ rows.0.id }},{{ this }}or{{ @index }}. In HTML every value is escaped, and there is no raw form, so data can never add markup.- A value is escaped for the place it goes: in text, in a quoted attribute such as
title="{{ name }}", or in an attribute without quotes. Text in data that looks like{{ }}or${{ }}stays plain text. - In a link or address attribute such as
hreforsrc, a value can only start a relative address or anhttp,https,mailtoorteladdress. Any other start, such asjavascript:ordata:, is replaced byabout:invalid#blocked, so the link does nothing. - In a link or address attribute, write the start of the address yourself, such as
href="https://{{ host }}/orders". A template can’t be saved when the data could still choose the kind of address: a value followed by a:(also written as:or:), such ashref="{{ scheme }}://example.com". Character references such asjcount as the character they stand for, so they can’t hide ajavascript:start either. Before a value, end a character reference with;. - Some places can’t take a value at all, because no escaping makes data safe there: inside
<script>or<style>, instyle,srcset,srcdocand event attributes such asonclick, and inside a tag outside an attribute value, such as<p {{ attrs }}>. A template with a value in one of these places can’t be saved, and the error names the line. To choose between fixed styles, use a block:<p {{#if late}}style="color: red"{{/if}}>. {{#each list}} ... {{/each}}repeats for each item. Inside it,{{ this }}is the item and{{ this.name }}is a field of the item. A name withoutthis., such as{{ name }}, always means the top-leveldata, also inside a loop. So a field in your data can never take the place of a top-level value with the same name. In a loop inside a loop,thisis the item of the inner loop, and{{#each this.lines}}repeats a list inside the item. An empty or missing list shows nothing.{{#if value}} ... {{else}} ... {{/if}}uses the same rules asif:. An empty list counts as false.- Errors name the line, for example “line 3: {{#if}} is never closed”.
- Limits: 512 KB of template, 16 nested blocks, 200 000 loop rounds and 8 MB of output.
Errors from built-in actions
Section titled “Errors from built-in actions”A built-in action fails the step with a message that is safe to show. It never contains your data or secret values. Wrong input fails at once and is not retried. Failures from the other system, such as a 429 or 5xx status or a timeout, can be retried with retry:. The step’s error code is shown in the run log. See Error codes.