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.