Skip to main content

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:

FieldMeaning
_typeOne of the nine built-in task types.
_nameUnique name within the workflow. Downstream tasks reference outputs by this name.
_sequencenoExecution order (ascending). Required on every task, including tasks nested inside FORK_JOIN, DO_WHILE, and SWITCH.
_inputParamsThe task's inputs. For SCRIPT_EXECUTION this includes _userType + _scriptName; other fields are passed to the worker.
_retryCountHow many times to retry on failure.
_timeoutSecondsPer-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.scriptOutput
  • REST_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:

FieldMeaningExample
_retryCountMaximum number of retries after the first attempt.3 → up to 4 attempts total.
_retryLogicHow the delay grows between attempts: FIXED, LINEAR, or EXPONENTIAL_BACKOFF."EXPONENTIAL_BACKOFF"
_retryDelaySecondsThe 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:

_retryLogicDelay before each retryWaits with base 5s
FIXEDAlways the base delay.5s, 5s, 5s, …
LINEARBase delay × attempt number.5s, 10s, 15s, …
EXPONENTIAL_BACKOFFBase 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 _retryCount and _timeoutSeconds on 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. _retryLogic and _retryDelaySeconds matter 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 APS REST_CONNECTOR + DO_WHILE pipeline. 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 (getParamsinput.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.