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.