Workflow file format
Who this is for: people who read or edit workflow YAML, and AI assistants that write it.
Before you start: it helps to know the words workflow, step, action and artifact from What Liitora does.
A workflow is one YAML file shaped like a GitHub Actions workflow: when it runs (on:), then a list of steps:. Each step uses a ready-made action (uses:) or runs a small JavaScript function (run:). This page lists every key Liitora accepts. Unknown keys are refused, and the message says which key you probably meant.
A complete example
Section titled “A complete example”This example is checked by Liitora’s validator. The comments explain each part.
# Required: a plain-language name and description.name: Overdue devices follow-updescription: Every weekday morning, fetch the overdue devices from the ERP, email IT a CSV of them and open one service ticket per device.
on: schedule: - cron: "0 8 * * 1-5" # 08:00, Monday to Friday timezone: Europe/Helsinki # local time, including summer time calendar: skip_on: [fi_public_holidays] shift: next_business_day # a holiday run moves to the next business day webhook: endpoint: devices-changed # also run when this webhook is called manual: true # people can start it with Run now
concurrency: group: overdue-devices # never two runs at the same time cancel_in_progress: false # a new run waits for the running one
expect: # alert if no run succeeded by 09:00 succeeds_by: "09:00" on: weekdays timezone: Europe/Helsinki skip_on: [fi_public_holidays]
steps: - id: fetch name: Fetch overdue devices from the ERP uses: platform/http.request@v1 connections: api: internal-erp # the connection holds the address and sign-in with: method: GET url: /v1/devices query: status: overdue retry: attempts: 3 backoff: exponential on: [429, 5xx, timeout, network] timeout_minutes: 5
- id: check name: Check whether any devices are overdue uses: platform/evaluate@v1 with: expression: ${{ steps.fetch.outputs.json.devices.length > 0 }}
- id: report name: Build a CSV of the overdue devices if: ${{ steps.check.outputs.result }} # skipped when nothing is overdue uses: platform/csv.build@v1 with: rows: ${{ steps.fetch.outputs.json.devices }} columns: [id, owner, due_date] filename: overdue.csv
- id: email name: Email the report to IT uses: platform/email.send@v1 with: to: [it@example.com] subject: "Overdue devices on ${{ format_date(now(), '%Y-%m-%d', 'Europe/Helsinki') }}" text: "${{ steps.report.outputs.rows }} devices are overdue. The list is attached." attachments: ["${{ artifacts.overdue.csv }}"]
- id: tickets name: Open a service ticket for each device needs: [report] # runs next to email, not after it uses: platform/http.request@v1 connections: api: service-desk strategy: for_each: ${{ steps.fetch.outputs.json.devices }} max_parallel: 3 fail_fast: false with: method: POST url: /api/tickets json: title: "Return device ${{ each.item.id }}" owner: ${{ each.item.owner }}
- id: notify_failure name: Tell the IT channel that the follow-up failed needs: [email, tickets] if: ${{ failure() }} # runs only when an earlier step failed uses: platform/webhook.send@v1 connections: target: it-chat with: url: /hooks/it json: text: "The overdue devices follow-up failed. Open Liitora to see the run."Liitora turns this into the sentence people see on the approval page: “Every weekday at 08:00 Helsinki time, when the devices-changed webhook is called, or when someone starts it by hand: fetch overdue devices from the ERP, then check whether any devices are overdue. If that check passes, build a CSV of the overdue devices, then email the report to IT and open a service ticket for each device…”
Top-level keys
Section titled “Top-level keys”| Key | Required | What it is |
|---|---|---|
name |
yes | A plain-language name, one line, at most 120 characters. Names that look like ids, such as step1, are refused. |
description |
yes | What the workflow does and why, at most 2000 characters. |
on |
no | The triggers. Without any trigger you get the warning no_triggers: the workflow can then only be called by another workflow. |
runs-on |
no | Runs the workflow on your own runners: self-hosted/<group>. Without it, the workflow runs on Liitora. See Runs on. |
concurrency |
no | Keeps runs from overlapping. See Concurrency. |
expect |
no | A freshness check: the workflow must succeed by a time of day. See Expect. |
steps |
yes | The list of steps, at least 1 and at most 100. See Steps. |
Triggers
Section titled “Triggers”on: says what starts a run. You can combine several triggers in one workflow.
| Key | Value | Starts a run when |
|---|---|---|
schedule |
a list of schedules | the time comes. See Schedule. |
webhook |
{ endpoint: <name> } |
another system calls the webhook endpoint with that name. |
workflow_run |
{ workflow: <name>, types: [...] } |
another workflow of your company finishes. |
manual |
true, {} or empty |
a person selects Run now in the app. |
api |
true, {} or empty |
a program starts it through the Liitora API. |
manual: false and api: false are the same as leaving the key out. Liitora checks both keys in the active version:
- Without
manual, Run now is refused withmanual_start_not_allowed. - Without
api, a production run started with an API token or by an AI assistant is refused withapi_start_not_allowed.
Test runs and dry runs always work, so you can try a workflow before you decide how it starts. To allow a start, add the key, save and activate the new version.
Schedule
Section titled “Schedule”Each entry has these keys. A workflow can have at most 10 schedules.
| Key | Required | Default | What it is |
|---|---|---|---|
cron |
yes | none | Five fields: minute, hour, day of month, month, day of week. |
timezone |
no | UTC |
An IANA time zone name, such as Europe/Helsinki or Europe/Stockholm. |
calendar |
no | none | Holidays and blackout days. See Calendar. |
misfire |
no | skip |
What happens to times missed while Liitora couldn’t run the schedule: skip or run_once. See Missed runs below. |
Cron rules:
- Exactly five fields. Seconds, years and shortcuts such as
@dailyare refused. - Ranges (
1-5), lists (1,3,5) and steps (*/15) work. Day of week0is Sunday and1-5is Monday to Friday. - Runs must be at least your plan’s minimum interval apart (5 minutes unless your plan says otherwise). Liitora checks the shortest gap, including summer time changes.
- Quote the pattern (
"0 8 * * 1-5"), because*has a meaning in YAML.
| Pattern | Runs |
|---|---|
"0 8 * * 1-5" |
at 08:00 on weekdays |
"30 6 * * *" |
at 06:30 every day |
"0 7 * * 1" |
at 07:00 on Mondays |
"*/15 * * * *" |
every 15 minutes |
"0 9 1 * *" |
at 09:00 on the first day of each month |
Summer time. Times are local to timezone. When clocks move forward, a fixed time in the missing hour (for example 03:30) runs once, at the first valid moment after it. When clocks move back, a fixed time in the repeated hour runs once, the first time. Interval schedules such as */15 follow real time: they skip the missing hour and run twice in the repeated hour.
Missed runs. If a run could not start at its time for more than 5 minutes (for example during maintenance), it counts as missed and an alert is recorded. With misfire: skip (the default) missed times don’t run. With misfire: run_once, Liitora makes one catch-up run for all of them, for the latest missed time. Use run_once when the work must happen even if it is late, such as a nightly export.
on: schedule: - cron: "0 2 * * *" timezone: Europe/Helsinki misfire: run_onceCalendar
Section titled “Calendar”calendar: belongs to one schedule.
| Key | Default | What it is |
|---|---|---|
skip_on |
none | A list of calendars whose days block the run: fi_public_holidays, se_public_holidays, weekends, tenant_blackouts. Each at most once. |
shift |
none |
none skips a blocked run. next_business_day runs it at the same local time on the next business day instead. |
A business day is Monday to Friday and not blocked by skip_on. If several runs move to the same day and time, they become one run. shift: next_business_day without skip_on gives the warning shift_without_skip_on. The how-to Use calendars and holidays lists which holidays each calendar has.
Webhook
Section titled “Webhook”on: webhook: endpoint: devices-changedendpoint is the name of a webhook endpoint in your company: lowercase letters, digits and -. The run gets the delivery in trigger: trigger.body (the parsed JSON, the form fields or the text; in JSON, whole numbers of 2^53 (9,007,199,254,740,992) or more come as text so no digit is lost), trigger.headers, trigger.content_type, trigger.received_at, trigger.event_id and trigger.idempotency_key. A form body is always split into its fields when the run starts, even without parse, and trigger.raw_body is the text as sent. See Set up a webhook.
parse reads a text body for you, so steps can use its fields directly:
on: webhook: endpoint: orders-in parse: xml # or csv, or formparse |
trigger.body becomes |
|---|---|
xml |
the document as xml.parse gives it, such as trigger.body.order["@id"] |
csv |
a list of rows, one object per row, with the first row as the column names |
form |
the form fields, with a repeated field as a list |
trigger.raw_body is always the text as sent, so a script can check a signature or read it another way. The body is read when the run starts, with the same rules and limits as ctx.formats in scripts. If it can’t be read (for example the XML is not well-formed, a form has a broken % code, or the sender sent JSON), the run fails with format_error and is not retried. Leave parse out to get JSON as an object and other text as it is.
receive: runner lets the workflow’s runner receive the webhook instead of Liitora, so the body and the headers never enter Liitora. It needs runs-on: self-hosted/<group>. The endpoint and its secret are then set on the runner, not in Liitora, and the run gets the delivery in trigger as above. Leave receive out, or write receive: liitora, to receive the webhook on Liitora. See Receive webhooks inside your network.
runs-on: self-hosted/on-premon: webhook: endpoint: erp-orders receive: runnerWorkflow run
Section titled “Workflow run”on: workflow_run: workflow: nightly-device-sync types: [succeeded]| Key | Default | What it is |
|---|---|---|
workflow |
none | The other workflow’s name (its slug) in your company. |
types |
[succeeded] |
Which results start this workflow: succeeded, failed, cancelled or any. any can’t be combined with others. |
The run gets trigger.workflow_run with id, workflow_id, result and finished_at of the run that finished. Test runs never start other workflows.
What every run gets in trigger
Section titled “What every run gets in trigger”Besides the trigger’s own values, every run has trigger.kind, trigger.now (the time the run is for), trigger.scheduled_for, trigger.run_id, trigger.environment (production or test), trigger.is_test and trigger.payload. A scheduled run also has trigger.cron, trigger.timezone and trigger.slot, and trigger.shifted_from when a calendar moved it. A run started with Run now gets the JSON input you typed.
Runs on
Section titled “Runs on”runs-on: self-hosted/<group> sends every run of the workflow to the runners of that runner group in your own network. Only runners of the group ever run it: when none is online, runs wait and admins get the runner_unavailable alert. They never move to Liitora’s own workers.
- The group name has 1 to 63 lowercase letters, digits and hyphens, and starts with a letter or digit. Any other value is refused with
invalid_runs_onorinvalid_runner_group. - The group must exist when you save the workflow (
runner_group_missing). - Secrets come from the runner’s own environment, and some features are not available on runners yet.
See Run automations in your own environment.
Concurrency
Section titled “Concurrency”concurrency: group: overdue-devices cancel_in_progress: false| Key | Default | What it is |
|---|---|---|
group |
none | Runs with the same group never run at the same time. It may use ${{ trigger... }} and ${{ vars... }}, for example sync-${{ trigger.body.region }}. It can’t be empty. |
cancel_in_progress |
false |
false: a new run waits until the running one ends. true: the running one is cancelled. |
Expect
Section titled “Expect”expect: is a freshness check. If no run of this workflow succeeded by the deadline, an alert is raised, whether the run failed, was late or never started. See Set freshness expectations.
| Key | Required | Default | What it is |
|---|---|---|---|
succeeds_by |
yes | none | The deadline, HH:MM local time, 00:00 to 23:59. Quote it: "09:00". |
from |
no | "00:00" |
When the window opens, HH:MM. A time not before succeeds_by opens the window on the previous day, for example from: "18:00" with succeeds_by: "09:00". |
on |
no | daily |
Which days have a deadline: daily, weekdays, weekends, or a list such as [mon, wed, fri]. |
timezone |
no | UTC |
An IANA time zone. |
skip_on |
no | none | Calendars whose days have no deadline, as in Calendar. |
A success exactly at the deadline counts. Test runs never count.
| Key | Required | Default | What it is |
|---|---|---|---|
id |
yes | none | A short id: starts with a lowercase letter, then lowercase letters, digits, _ or -, at most 64 characters. Unique in the workflow. Other steps use it in needs: and expressions. |
name |
yes | none | A plain-language name on one line, at most 120 characters. |
uses |
one of uses and run |
none | The action to run, such as platform/http.request@v1. See Action references. |
run |
one of uses and run |
none | An inline JavaScript module, at most 64 KB. See Host API for scripts. |
with |
no | none | The inputs of the action (or of the script). Values may contain expressions. Checked against the action’s input schema when you save. |
connections |
no | none | Maps the action’s connection slots to your connections, for example api: internal-erp. |
needs |
no | the step above | One step id or a list. needs: [] means no dependency. |
if |
no | success() |
A condition. See Conditions and dependencies. |
strategy |
no | none | Runs the step once per item. See For each. |
retry |
no | no retries | Tries a failed step again. See Retry. |
timeout_minutes |
no | 15 |
Stops one attempt after this many minutes, 1 to 60. |
continue_on_error |
no | false |
true: a failure of this step does not fail the run, and later steps treat it as a success. |
wait |
no | true |
Only for workflow calls (uses: workflow/...): true waits for the called workflow and returns its result; false starts it and moves on. |
Outputs and files
Section titled “Outputs and files”A step’s result is steps.<id>.outputs. For built-in actions the fields are in Built-in actions. For a run: script, the object it returns is its outputs. steps.<id>.result is success, failure, skipped or cancelled.
Files a step makes (artifacts) are available to later steps as artifacts.<file name>, for example ${{ artifacts.overdue.csv }}. Each artifact has id, name, content_type and size_bytes. File names are unique within a run.
Inline scripts
Section titled “Inline scripts” - id: fetch name: Fetch invoices changed since the last sync run: | export default async function run(ctx, input) { const erp = ctx.connection("erp"); const res = await erp.get("/v1/invoices", { query: { since: input.since } }); if (!res.ok) throw new Error(`the ERP answered ${res.status}`); const items = res.json().items; if (items.length === 0) return ctx.skip("no new invoices"); return { items }; } connections: erp: visma with: since: ${{ steps.cursor.outputs.value }}input holds the step’s with: values. Logic that other workflows could reuse belongs in an action in your library, with its own schemas and tests.
Conditions and dependencies
Section titled “Conditions and dependencies”Steps run in the order you write them, unless needs: says otherwise:
- A step without
needs:waits for the step above it. The first step starts at once. needs: [a, b]waits for bothaandb. Steps that don’t depend on each other run side by side.needs: []starts the step at once.- Unknown ids, a step that needs itself, a repeated id and loops are errors. A loop is shown as
a -> b -> a.
if: decides whether a step runs. You can write it with or without ${{ }}, but not mix text and ${{ }} (that would always be true).
- No
if:meanssuccess(): the step runs only when every step it depends on succeeded. - An
if:without a status function meanssuccess() && (<your condition>). Your condition is not evaluated when an earlier step failed or was skipped. - So a skipped or failed step also skips the steps that depend on it, as in GitHub Actions. To run anyway, use a status function.
Status functions
Section titled “Status functions”These work only in if:.
| Function | True when |
|---|---|
success() |
every earlier step this step depends on (directly or through others) succeeded, and the run is not cancelled |
failure() |
an earlier step this step depends on failed |
always() |
always, even after failures or a cancel |
cancelled() |
the run was cancelled |
Useful patterns:
if: |
Runs when |
|---|---|
${{ failure() }} |
something earlier failed, for example to send an alert |
${{ always() }} |
always, for example to clean up |
${{ !failure() && !cancelled() }} |
nothing failed, even if some steps were skipped |
${{ always() && needs.check.result == 'skipped' }} |
a direct dependency was skipped |
A step with continue_on_error: true that fails counts as a success for the steps after it. In a cancelled run, steps without always() or cancelled() are marked cancelled.
For each
Section titled “For each” strategy: for_each: ${{ steps.fetch.outputs.json.devices }} max_parallel: 3 fail_fast: false| Key | Default | What it is |
|---|---|---|
for_each |
none | An expression that gives a list. The step runs once per item, at most 1000 items. |
max_parallel |
your plan’s cap (5 by default) | How many items run at the same time, 1 to 20, and never more than your plan allows. |
fail_fast |
true |
true: after the first failed item no new items start and the rest are cancelled. false: every item runs. |
quarantine |
false |
true: an item that still fails after its retries is set aside (quarantined) instead of failing the step. Every item runs, later steps run, and the run ends as failed with items_quarantined. If every item fails, the step fails as usual. See Replay quarantined items. |
Inside with: of that step, each.item is the current item and each.index its position (from 0). The step’s outputs are a list with one entry per item, in order, and null for items that did not succeed. An empty list is a success.
retry: attempts: 3 backoff: exponential on: [429, 5xx, timeout, network]| Key | Default | What it is |
|---|---|---|
attempts |
none (required) | Total attempts including the first, 1 to 10. |
backoff |
exponential |
The wait between attempts: fixed, linear or exponential, from 1 second up to 60 seconds. |
on |
[429, 5xx, timeout, network] |
Which failures are retried: an HTTP status such as 429 or 503, 4xx, 5xx, timeout or network. Can’t be empty. |
Wrong input, refused calls and limits that were reached are never retried. Every attempt sends the same Idempotency-Key, so the other system can tell a retry from a new request.
Workflow calls
Section titled “Workflow calls”A step can call another workflow of your company:
- id: tickets name: Open the service tickets uses: workflow/acme/open-service-tickets@v3 wait: true with: devices: ${{ steps.fetch.outputs.json.devices }}- The owner is your company’s short name. You can only call your own workflows.
- In production, the called workflow’s active version runs, never an unapproved one. Test runs call the version you pin.
wait: true(the default) waits and returns the called run’s result,run_idandstatus.wait: falsereturnsrun_idandstatus: queuedat once.- Calls can be nested 5 levels deep, and a workflow can’t call itself through others.
- A waiting call keeps a run slot busy, so your plan must allow at least 2 runs at the same time.
Expressions
Section titled “Expressions”${{ ... }} computes a value. You can use it in with:, if:, strategy.for_each and concurrency.group.
- A value that is only one expression keeps its type:
rows: ${{ steps.fetch.outputs.json.devices }}stays a list. - Text around an expression makes text:
"Found ${{ steps.report.outputs.rows }} rows". There,nullbecomes empty text and lists or objects become compact JSON.
What you can refer to
Section titled “What you can refer to”| Name | What it is |
|---|---|
trigger |
The run’s trigger payload (see Triggers). |
steps.<id>.outputs, steps.<id>.result |
A step that runs before this one (directly or through others). |
needs.<id>.outputs, needs.<id>.result |
A direct dependency listed in needs:. |
artifacts.<file name> |
A file made earlier in this run. The longest matching file name wins, so artifacts.overdue.csv finds overdue.csv. A run can’t have two files where one name is the other plus a dot and more, such as report and report.id, so a name always means one file. |
each.item, each.index |
The current item, only in with: of a for-each step. |
vars.<name> |
A company variable, such as vars.report_to. A name that doesn’t exist is empty (null). Runs read the values when they start. |
In concurrency.group only trigger and vars are allowed.
Syntax
Section titled “Syntax”- Literals:
null,true,false, numbers,'single quoted'(write''for a quote) and"double quoted"text. - Access:
a.b,a[0],a['key with spaces'],a.0..lengthworks on lists and text. - Operators, from weakest to strongest:
? :,||,&&,==!=,<<=>>=,+-,*/%,!and minus. - Pipe:
x | f(a)is the same asf(x, a), for example${{ steps.fetch.outputs.json.name | lower() }}.
How values behave:
- A missing property is
null, not an error. &&and||return one of their operands, as in GitHub Actions. False values arefalse,0,''andnull.==compares values exactly: numbers by value, no conversion between types.<and the other comparisons work between two numbers or two texts.+adds numbers or joins two texts. Dividing by zero is an error.- Whole numbers are exact up to 64 bits, so a large ID such as
9007199254740993keeps every digit through functions, arithmetic and comparisons. A result that is too large for that, or has a fraction, is a decimal number. A result too large for any number is an error. - Limits: an expression is at most 4096 characters, 32 levels deep and 100 000 evaluation steps. Texts are at most 1 MB and lists at most 10 000 items. One expression can work on at most 64 MB of data in total. An expression that would pass a limit stops with
evaluation_erroras soon as it gets there. To handle more data, refer only to the fields you need, or use aplatform/transformstep.
Functions
Section titled “Functions”| Function | Returns | Example |
|---|---|---|
length(value) |
Items in a list, characters in text or keys in an object; 0 for null. | length(steps.fetch.outputs.json.devices) |
contains(search, item) |
True when a list has an equal item, or text contains the text (ignoring case). | contains(trigger.headers['x-event'], 'device') |
startsWith(text, prefix) |
True when the text starts with the prefix (ignoring case). | startsWith(trigger.body.id, 'INV-') |
endsWith(text, suffix) |
True when the text ends with the suffix (ignoring case). | endsWith(trigger.body.email, '@example.com') |
join(list, separator = ',') |
The items as text, with the separator between them. | join(steps.fetch.outputs.json.ids, ', ') |
split(text, separator) |
The text cut into a list. | split('a;b;c', ';') |
toJSON(value) |
The value as compact JSON text. | toJSON(trigger.body) |
fromJSON(text) |
JSON text parsed into a value. | fromJSON(steps.read.outputs.body) |
format(template, args...) |
{0}, {1} and so on replaced by the arguments. Write {{ and }} for braces. At most 15 arguments. |
format('{0} of {1}', 3, 10) |
default(value, fallback) |
The value, or the fallback when the value is null or empty text. | default(trigger.body.region, 'fi') |
lower(text) |
The text in lower case. | lower(trigger.body.status) |
upper(text) |
The text in upper case. | upper('fi') |
trim(text) |
The text without spaces at the start and end. | trim(trigger.body.name) |
url_encode(text) |
The text made safe for one part of a URL, such as a path segment or a query value. Every character except the letters a to z and A to Z, the digits and -, ., _ and ~ is percent-encoded, so ?, &, / and % stay inside the value. |
format('/customers/{0}', url_encode(trigger.body.id)) |
now() |
The current time, RFC 3339 in UTC. Frozen in tests. | now() |
plus_days(date, days, timezone?) |
The date moved forward by whole calendar days. With a time zone, days are counted in that zone across summer time. | plus_days(now(), 7, 'Europe/Helsinki') |
minus_days(date, days, timezone?) |
The date moved back by whole calendar days. | minus_days(now(), 1) |
plus_hours(date, hours) |
The time moved forward by hours. | plus_hours(trigger.now, 2) |
minus_hours(date, hours) |
The time moved back by hours. | minus_hours(now(), 24) |
format_date(date, format, timezone?) |
The date formatted with strftime codes such as %Y-%m-%d, in the time zone if one is given. |
format_date(now(), '%d.%m.%Y', 'Europe/Helsinki') |
lookup('table', key, column?, fallback?) |
The row of a lookup table whose key (first column) is the key as exact text, as an object of column to text. With a column, only that cell. An unknown key is an error unless a fallback is given. The table name must be in quotes. Runs read the version that was current when they started. | lookup('cost_centers', trigger.body.code, 'name') |
Use url_encode whenever a value you don’t control goes into a connection path. Without it, an ID such as 42?admin=1 or a/b would change which address is called. A path whose value is encoded more than once to hide .. is refused.
Dates are RFC 3339 texts (2026-10-05T07:00:00Z) or plain dates (2026-10-05), years 1 to 9999. The status functions success(), failure(), always() and cancelled() are described in Status functions.
What is checked when you save
Section titled “What is checked when you save”- Syntax, unknown names and functions, and the number of arguments.
lookup()must name its table in quotes. A table that doesn’t exist yet is a warning (unknown_lookup_table), not an error, so you can create it afterwards.steps.<id>must exist, must not be the step itself, and must run before it.needs.<id>must be a direct dependency.eachonly inwith:of a for-each step, status functions only inif:.- Values are checked against the action’s input schema: missing and unknown inputs, types, allowed values and lengths. An expression’s type is checked too, when it is known. References to another step’s outputs are checked when that action lists its outputs.
YAML rules
Section titled “YAML rules”- One document per file, at most 512 KB, at most 32 levels deep and 50 000 values.
- Anchors, aliases (
&x,*x), tags (!!str) and complex keys are refused. Repeated keys are refused. - Liitora reads YAML 1.2, so
on:stays a key and is not read astrue. - Keys use underscores:
timeout_minutes, nottimeout-minutes. - Every problem is reported with its line and column, what went wrong and how to fix it.
Validation messages
Section titled “Validation messages”Every message has a code, a place, what went wrong and how to fix it. Errors stop the save. Warnings are kept with the version.
| Code | What it means |
|---|---|
yaml_syntax |
The file is not valid YAML. |
yaml_empty |
The file is empty. |
yaml_multiple_documents |
The file has more than one YAML document (---). |
yaml_too_large |
The file is larger than 512 KB or has too many values. |
yaml_too_deep |
The file is nested more than 32 levels deep. |
yaml_unsupported |
The file uses anchors, aliases, tags or complex keys. |
yaml_duplicate_key |
A key appears twice in the same place. |
not_a_mapping |
The file (or a part of it) is not a set of keys and values. |
unknown_key |
A key Liitora doesn’t know. The message suggests the right one, also for GitHub Actions keys. |
missing_key |
A required key is missing, such as cron in a schedule. |
invalid_value |
A value has the wrong type, for example text where a number belongs. |
missing_name |
The workflow or a step has no name:. |
missing_description |
The workflow has no description:. |
name_not_plain_language |
A name looks like an id, such as step1. Use words people understand. |
text_too_long |
A name or the description is too long. |
invalid_text |
A name spans several lines or has control characters. |
no_steps |
The workflow has no steps. |
too_many_steps |
More than 100 steps. |
no_triggers |
Warning: nothing starts the workflow, so only another workflow can call it. |
too_many_schedules |
More than 10 schedules. |
invalid_cron |
The schedule is not a five-field cron pattern. |
invalid_timezone |
Not an IANA time zone. The message suggests close names. |
schedule_too_frequent |
Runs would be closer together than your plan allows. |
unknown_calendar |
skip_on names a calendar that doesn’t exist. |
duplicate_calendar |
A calendar is listed twice. |
unknown_shift |
shift is not none or next_business_day. |
unknown_misfire |
misfire is not skip or run_once. |
shift_without_skip_on |
Warning: shift: next_business_day has no skip_on, so nothing moves. |
invalid_time_of_day |
succeeds_by or from is not HH:MM between 00:00 and 23:59. |
invalid_expect_days |
expect.on is not a preset or a list of different day names. |
invalid_webhook_endpoint |
The webhook endpoint name has characters other than lowercase letters, digits and -. |
unknown_webhook_parse |
parse is not xml, csv or form. |
unknown_webhook_receiver |
receive is not liitora or runner. |
webhook_receiver_needs_runner |
receive: runner on a workflow without runs-on: self-hosted/<group>. |
invalid_workflow_run |
The workflow_run trigger names no valid workflow, or types is empty or mixes any with others. |
empty_concurrency_group |
concurrency.group is empty. |
missing_id |
A step has no id:. |
invalid_id |
A step id breaks the id rules. |
duplicate_id |
Two steps have the same id. |
missing_action |
A step has neither uses: nor run:. |
both_uses_and_run |
A step has both uses: and run:. |
empty_run |
run: is empty. |
run_too_large |
run: is larger than 64 KB. |
script_expression |
A platform/transform script uses a ${{ }} expression. Pass the data through input instead. |
html_expression |
The html of a platform/email.send step uses a ${{ }} expression. Store the HTML as a template and pass the values through data, or send plain text. |
invalid_action_ref |
uses: is not a valid action reference. |
unknown_need |
needs: names a step that doesn’t exist. |
self_need |
A step needs itself. |
duplicate_need |
A step id is listed twice in needs:. |
dependency_cycle |
Steps wait for each other in a loop. |
chain_too_deep |
The longest chain of steps is longer than 50. |
invalid_retry_attempts |
retry.attempts is below 1 or above 10. |
empty_retry_on |
retry.on is an empty list. |
invalid_retry_on |
retry.on has something other than a status, 4xx, 5xx, timeout or network. |
invalid_timeout |
timeout_minutes is below 1 or above 60. |
invalid_max_parallel |
max_parallel is below 1 or above the limit. |
for_each_not_a_list |
for_each does not give a list. |
wait_without_workflow |
wait: is set on a step that doesn’t call a workflow. |
invalid_connection_name |
A connection name has characters that are not allowed. |
invalid_connection_slot |
A connection slot name has characters that are not allowed. |
unknown_connection_slot |
The action has no connection slot with that name. |
missing_connection |
The action needs a connection for a slot, and none is given. |
unknown_input |
with: has an input the action doesn’t have. The message suggests close names. |
missing_input |
A required input is not set. |
invalid_input |
An input breaks the action’s rules: allowed values, length or number range. |
input_type_mismatch |
An input has the wrong type, such as a number where text belongs. |
invalid_expression |
An expression has a syntax error. |
unknown_name |
An expression uses a name that doesn’t exist. |
unknown_function |
An expression calls a function that doesn’t exist. |
wrong_argument_count |
A function gets too few or too many arguments. |
status_function_outside_if |
success(), failure(), always() or cancelled() is used outside if:. |
root_not_allowed |
A name is not available in that place, such as steps in concurrency.group. |
unknown_step_reference |
steps.<id> names a step that doesn’t exist. |
self_reference |
A step reads its own result. |
step_not_upstream |
A step reads a step that doesn’t run before it. Add it to needs:. |
not_a_need |
needs.<id> is used for a step that is not in needs:. |
unknown_step_property |
Only outputs and result exist under a step. |
unknown_output |
The action has no output with that name. The message lists the outputs. |
each_outside_for_each |
each is used in a step without strategy.for_each. |
unknown_each_property |
Only each.item and each.index exist. |
evaluation_error |
An expression could not be computed, for example a division by zero. |
workflow_call_cycle |
Workflows call each other in a loop. |
workflow_call_too_deep |
Workflow calls are nested too deep. |
When you save, Liitora also checks the scripts and scans the values for passwords and keys. Those messages are listed in Host API for scripts. Saving can also warn that an action, a called workflow or a connection doesn’t exist yet (unknown_action, unknown_workflow, unknown_connection). You can save such a draft, but you can’t activate it until they exist.