Tasks, task I/O, and expression bindings
The previous lesson ran a workflow end to end. This lesson focuses on the single most important mechanic in the service: how a task is defined, how it receives input, and how its output reaches the next task.
Anatomy of a Task Definition#
Every entry in a workflow definitions _taskDefs is a task definition. The common fields are:
| Field | Meaning |
|---|---|
_type | One of the nine built-in task types. |
_name | Unique name within the workflow. Downstream tasks reference outputs by this name. |
_sequenceno | Execution order (ascending). Required on every task, including tasks nested inside FORK_JOIN, DO_WHILE, and SWITCH. |
_inputParams | The task's inputs. For SCRIPT_EXECUTION this includes _userType + _scriptName; other fields are passed to the worker. |
_retryCount | How many times to retry on failure. |
_timeoutSeconds | Per-task time budget. |
A SCRIPT_EXECUTION task, the type used most in this course, always identifies the function to run:
{ _type: "SCRIPT_EXECUTION", _name: "analyze_weather", _sequenceno: 2, _timeoutSeconds: 30, _inputParams: { _userType: "weather_workflow_demo_static", // which script _scriptName: "analyzeWeatherSeverity", // which function in it weatherData: "${fetch_weather._output.scriptOutput}" // an input binding }}_userType + _scriptName locate the function; every other key in
_inputParams becomes an input to it.
The Three Binding Data Forms#
Tasks pass data by reference. A binding is a ${...} (or $....) string that the runtime resolves to a concrete value just before the task runs.
Workflow-Level Input#
You can define default data for and pass data to a Workflow Run at runtime.
Defautl data is declared in the Workflow Definition's _inputParams and supplied when the run starts. For example, a workflow can declare it's -inputParams to include a city parameter:
{ "_name": "Weather_Alert_Router_Switch", "_description": "Route weather alerts based on severity using Switch pattern", "_namespaces": [], "_userType": "weather_workflow_router_static", "_taskDefs": [...], "_timeoutSeconds": 300, "_timeoutPolicy": "TIME_OUT_WF", "_inputParams": { "city": { "type": "string", "value": "New York" } }}When the Workflow is run, if another value for city has not been provided at runtime, 'New York' will be used.
The schema for _inputParams is { type, value, encrypt? }.
value is the default; a run can override it at run time by passing inputParams to the workflow.
A task references the city parameter as ${workflow.input.city}.
"_taskDefs": [ { "_id": "4dc084c2-01b7-4b49-b243-cbeb236b1609", "_name": "fetch_weather_for_city", "_type": "SCRIPT_EXECUTION", "_sequenceno": 1, "_inputParams": { "_userType": "weather_workflow_demo_static", "_scriptName": "generateNewYorkWeatherData", "cityToFetch": "${workflow.input.city}" }, "_retryCount": 2, "_timeoutSeconds": 30 }]Upstream Task Output#
A task can reference the resolved output of an upstream task as well.
Different task types expose their output to downstream tasks differently:
SCRIPT_EXECUTION→{{task_name}}._output.scriptOutputREST_CONNECTOR→{{task_name}}._output.result(or.body)USER_INPUT→{{task_name}}._output._userInput.<field>
For instance the return value of a SCRIPT_EXECUTION type task can be passed to the next task in the sequence like:
"_inputParams": { "convertedData": "${convertData.input._output}"},If only some of the output is needed for the task you can pass only the necessary output as an alternative:
"_inputParams": { "convertedCelsius": "${convertData.input._output.celsius}"},Task Local Variable#
Used only when evaluating a DO_WHILE loop condition, where the expression reads from the task's own resolved _inputParams. This will covered in detail in Polling loops with DO_WHILE.
How a Script Receives its Inputs#
Inside a SCRIPT_EXECUTION function, the resolved _inputParams arrive under input.actualParams.
So a SCRIPT_EXECUTION task with this _inputParam:
"_inputParams": { "convertedCelsius": "${convertData.input._output.celsius}"},can access convertedCelsius as:
async function aggregateWeatherResults(input, libraries, ctx) {
const convertedC = input.actualParams.convertedCelsius
}Reliability: Retries and Timeouts#
Two controls make a task robust against transient failure, declared directly on the task. The demo workflows set the two most common ones — a retry count and a timeout:
{ _type: "SCRIPT_EXECUTION", _name: "fetch_newyork_weather", _sequenceno: 1, _retryCount: 2, // retry up to twice on failure _timeoutSeconds: 30, // fail the task if it runs longer than 30s _inputParams: { ... }}The Full Set of Retry Fields#
Three fields together define a retry policy:
| Field | Meaning | Example |
|---|---|---|
_retryCount | Maximum number of retries after the first attempt. | 3 → up to 4 attempts total. |
_retryLogic | How the delay grows between attempts: FIXED, LINEAR, or EXPONENTIAL_BACKOFF. | "EXPONENTIAL_BACKOFF" |
_retryDelaySeconds | The base delay, in seconds, that _retryLogic scales. | 5 |
_retryCount alone retries immediately, back-to-back. That is fine for a flaky
in-process script, but for a network call or a rate-limited API you usually want
to space the retries out.
How Each _retryLogic Computes the Delay#
Given _retryDelaySeconds: 5, the wait before attempt n is:
_retryLogic | Delay before each retry | Waits with base 5s |
|---|---|---|
FIXED | Always the base delay. | 5s, 5s, 5s, … |
LINEAR | Base delay × attempt number. | 5s, 10s, 15s, … |
EXPONENTIAL_BACKOFF | Base delay × 2^(attempt−1). | 5s, 10s, 20s, 40s, … |
Rules of thumb:
FIXED— a source that recovers on a predictable interval (a service that restarts every few seconds). Simplest, and fine when you have no reason to back off harder.LINEAR— a source under mild, recovering load; each retry gives it a little more room without waiting excessively.EXPONENTIAL_BACKOFF— the default choice for external HTTP APIs, and especially anything rate-limited (HTTP 429) or returning transient 5xx. Backing off exponentially avoids hammering an already-struggling service and is what the polling case in Lesson 13 uses.
Examples#
A resilient external call — four attempts, backing off 5s → 10s → 20s:
{ _type: "REST_CONNECTOR", _name: "fetch_forecast", _sequenceno: 1, _retryCount: 3, _retryLogic: "EXPONENTIAL_BACKOFF", _retryDelaySeconds: 5, _timeoutSeconds: 30, // caps EACH attempt, not the whole task _inputParams: { /* ... */ }}A steady poll — retry every 10s, up to five times:
{ _type: "SCRIPT_EXECUTION", _name: "poll_status", _sequenceno: 2, _retryCount: 5, _retryLogic: "FIXED", _retryDelaySeconds: 10, _timeoutSeconds: 15, _inputParams: { /* ... */ }}Timeouts and the timeout policy#
_timeoutSeconds on a task caps a single attempt: if the attempt runs longer, it fails (and may then be retried, subject to _retryCount). At the workflow level, _timeoutPolicy decides what a breach of the workflow's _timeoutSeconds means:
TIME_OUT_WF— terminate the whole run (the policy every demo workflow uses).ALERT_ONLY— record a counter and let the run continue.
Interaction to keep in mind#
Retries multiply worst-case time. A task with _timeoutSeconds: 30,
_retryCount: 3, and exponential back-off of base 5s can take up to
4 × 30s of attempts plus 5 + 10 + 20s of waiting (over two minutes) before it finally fails. Make sure the workflow-level _timeoutSeconds is large enough to accommodate the retry budget of its longest task, or TIME_OUT_WF will cut the run off mid-retry.
Where you will see these fields in this course. The demo workflows set
_retryCountand_timeoutSecondson their fetch tasks (see Lesson 03), but keep the default retry logic — the static data never actually fails, so there is nothing to back off from._retryLogicand_retryDelaySecondsmatter once a task talks to something external: Lesson 08 explains how they set a polling loop's cadence, and Lesson 13 shows them on the real Autodesk APSREST_CONNECTOR+DO_WHILEpipeline. This section is the reference for the fields themselves.
A note on writing scripts robustly#
The task scripts in this course defensively coerce inputs. For example,
splitCitiesIntoBatches accepts cities that may arrive as an array or as a JSON string, depending on how the value reached it:
let cities = p.cities || [];if (typeof cities === "string") { try { cities = JSON.parse(cities); } catch (e) { cities = []; }}This kind of guard is worth copying: a task script is a boundary where a binding resolves to a value you did not construct, so validating and normalising inputs prevents a whole class of runtime surprises.
Checkpoint#
You can now read any task definition in the demo file and predict:
- which function it runs (
_userType+_scriptName), - where each input comes from (which binding),
- how the function reads those inputs (
getParams→input.actualParams), - and how its output is addressed downstream (
._output.scriptOutput).
With this in hand, the remaining lessons each introduce one control-flow task type by pointing at a workflow that uses it.