Workflow Concepts: Variables, Runtime Resolution & Graph Structure
This document is a consolidated reference covering variables (declaration,
special variables), how the runtime resolves them during workflow
execution, and the graph structure of a workflow itself (Start/End/Links).
It merges the essential content of variables.md, runtime-resolver.md, and
introduction.md into a single reference.
1. What Variables Are
Variables are named containers that store data produced by tasks and can be used by subsequent tasks. They enable data flow and communication between different stages of a workflow.
There are two kinds of variables:
Declared variable ($NAME) |
Special variable (${NAME}) |
|
|---|---|---|
| Declaration | Required in Start.variables |
None — always available |
| Source of value | A task's output | Device/system state, resolved live |
| Fixed list | No — you choose the name | Yes — fixed set defined by the runtime |
| Typical use | Passing data between tasks | Timestamps, device info, enum comparisons |
2. Declaring Variables (START Node)
Variables must be declared in the START node:
- Naming: Must start with
$(mandatory prefix) - Scope: Available throughout the entire workflow
- Types supported:
String,Double,JSON
{
"Start": [
{
"id": "0",
"variables": [
{"variableName": "$PING_ERROR", "variableValue": "", "is_kpi": false},
{"variableName": "$NETWORK_STATUS", "variableValue": "unknown", "is_kpi": false},
{"variableName": "$TEMPERATURE", "variableValue": "0.0", "is_kpi": true}
]
}
]
}
Variable Types and Examples
| Type | Example | Initial variableValue |
Typical task output | Typical usage |
|---|---|---|---|---|
| String | $ERROR_MSG |
"" |
cmd_error_output |
CompareText, TextReport |
| Double | $TEMPERATURE |
"0.0" |
ntp_offset_output |
CompareNumber |
| JSON | $RESPONSE_DATA |
"{}" |
http_response_output |
Parse, extract |
3. Built-in Special Variables
Besides variables you declare yourself, AndroMate provides built-in special
variables resolved automatically by the runtime — no Start declaration, no
task needed to produce them.
Syntax is different from your own variables: special variables use
curly braces — ${NAME} — while your declared variables use a plain $
prefix — $NAME. The resolver (AndroMateContext.resolveSpecialVariables)
replaces any ${NAME} token it finds in a task parameter with its live value
at execution time, the same way it replaces $NAME with a value from the
runtime dictionary.
Use a special variable directly inside any parameter that accepts text — no
extra task, no Start variable required:
{
"SetAndromateVariable": [
{
"id": "1",
"variable_input": "$SMS_MESSAGE",
"variable_value": "Battery Level: $BATTERY_LEVEL%, Timestamp: ${CURRENT_DATE}"
}
]
}
Time
| Special variable | Resolved value | Format |
|---|---|---|
${CURRENT_TIME_STAMP} |
Current Unix epoch time in milliseconds | e.g. "1770963342000" |
${CURRENT_DATE} |
Current date and time on the device | "dd/MM/yyyy HH:mm:ss", e.g. "13/08/2026 09:15:42" |
${UPTIME_MS} |
Milliseconds since the device last booted (excludes deep sleep) | e.g. "48213045" |
${ELAPSED_REALTIME_MS} |
Milliseconds since boot, including deep sleep | e.g. "52007112" |
OS
| Special variable | Resolved value |
|---|---|
${CURRENT_SDK} |
Android SDK level, e.g. "34" |
${ANDROID_VERSION} |
Android release version, e.g. "14" |
${ANDROID_CODENAME} |
Android codename, e.g. "UpsideDownCake" |
Device
| Special variable | Resolved value |
|---|---|
${MANUFACTURER} |
Device manufacturer, e.g. "samsung" |
${MODEL} |
Device model, e.g. "SM-S911B" |
${DEVICE} |
Device codename |
${BRAND} |
Device brand |
${PRODUCT} |
Product name |
${BOARD} |
Board name |
${HARDWARE} |
Hardware name |
${HOST} |
Build host |
${ID} |
Build ID |
${TAGS} |
Build tags |
${TYPE} |
Build type |
${USER} |
Build user |
Boolean literals
| Special variable | Resolved value |
|---|---|
${TRUE} |
"true" |
${FALSE} |
"false" |
Enum comparison constants
Used to compare a task's output against a known enum value (e.g. in a
CompareStrings task) instead of hardcoding the string:
| Special variable | Resolved value | Produced by |
|---|---|---|
${CHARGING_TYPE_USB} |
"USB" |
GetChargingType |
${CHARGING_TYPE_AC} |
"AC" |
GetChargingType |
${CHARGING_TYPE_WIRELESS} |
"Wireless" |
GetChargingType |
${CHARGING_TYPE_NONE} |
"None" |
GetChargingType |
${BATTERY_HEALTH_GOOD} |
"Good" |
GetBatteryHealth |
${BATTERY_HEALTH_OVERHEAT} |
"Overheat" |
GetBatteryHealth |
${BATTERY_HEALTH_DEAD} |
"Dead" |
GetBatteryHealth |
${BATTERY_HEALTH_OVER_VOLTAGE} |
"Over Voltage" |
GetBatteryHealth |
${BATTERY_HEALTH_COLD} |
"Cold" |
GetBatteryHealth |
${BATTERY_HEALTH_UNKNOWN} |
"Unknown" |
GetBatteryHealth |
${BATTERY_STATUS_CHARGING} |
"Charging" |
GetBatteryStatus |
${BATTERY_STATUS_DISCHARGING} |
"Discharging" |
GetBatteryStatus |
${BATTERY_STATUS_FULL} |
"Full" |
GetBatteryStatus |
${BATTERY_STATUS_NOT_CHARGING} |
"Not Charging" |
GetBatteryStatus |
${BATTERY_STATUS_UNKNOWN} |
"Unknown" |
GetBatteryStatus |
4. How the Runtime Resolves Variables
The Runtime Resolver is the core engine that processes variables during
workflow execution. It manages the lifecycle of variables by registering
outputs, resolving parameters, and casting types. It keeps a runtime
dictionary in memory: { "$VAR_NAME": "current_value", ... }.
flowchart TD
Start["1. START NODE<br/>Variables declared<br/>Dictionary initialized"]
Task1["2. TASK EXECUTES<br/>Produces output"]
Register["3. REGISTRATION<br/>Output stored in dictionary<br/>{$VAR: 'value'}"]
Task2["4. NEXT TASK AWAITS<br/>Has parameters with variables<br/>e.g. text_x: '$VAR'"]
Resolve["5. RESOLUTION<br/>Scan parameters for $VAR / ${VAR}<br/>Replace with dictionary/live values"]
Cast["6. CASTING<br/>Convert String to target type<br/>Double, JSON, etc."]
Execute["7. TASK EXECUTES<br/>With resolved & casted values"]
End["8. END NODE<br/>Dictionary persists until workflow ends"]
Start --> Task1 --> Register --> Task2 --> Resolve --> Cast --> Execute --> End
Step 1 — Declaration & dictionary initialization
At the START node, declared variables seed the runtime dictionary with their
initial values, e.g. {"$TIME_OFFSET": "0", "$LATITUDE": "0.0"}.
Step 2 — Output registration
When a task executes and produces output, the resolver registers that output into the dictionary under the variable name given in the task's output parameter:
After execution: {"$TIME_OFFSET": "125"} (previous value overwritten).
Step 3 — Parameter resolution
Before a task executes, the resolver scans all of its parameters for
variable tokens and replaces them with the dictionary value ($NAME) or the
live device value (${NAME}):
A single parameter can contain multiple variables, all resolved in one
pass, e.g. "NTP Offset: $TIME_OFFSET ms | Latitude: $LATITUDE".
Step 4 — Type casting
Resolved values are always strings; the resolver then casts them to the type the parameter expects:
| From | To | Example |
|---|---|---|
| String | String | "Synchronized" — no conversion |
| String | Integer | "125" → 125 |
| String | Double | "25.5" → 25.5 |
| String | JSON | '{"status":"ok"}' → object |
| String | Boolean | "true" → true |
Error scenarios
- Undefined variable: a parameter references a variable never declared
in
Startand never produced by a prior task → the resolver cannot find it in the dictionary → execution stops with an error. - Type casting error: the resolved string value cannot be converted to
the parameter's expected type (e.g.
"abc"into aDoublefield) → execution stops with an error.
Where variables live
| Context | Where to find it | Example |
|---|---|---|
| Declaration | Start[0].variables array |
{"variableName": "$TIME_OFFSET", "variableValue": "0"} |
| Task output | Task's *_output parameter fields |
"ntp_offset_output": "$TIME_OFFSET" |
| Task input | Task's regular parameter fields | "num_x": "$TIME_OFFSET" |
| Runtime dictionary | In-memory only, not present in the workflow JSON | {"$TIME_OFFSET": "125"} |
5. Best Practices
- Declare all variables upfront in the START node — a variable used before it is declared or produced causes an execution error.
- Use descriptive names:
$PING_ERROR,$NETWORK_STATUS— not$x,$var1. - Initialize with type-appropriate defaults:
""for String,"0.0"for Double,"{}"for JSON. - Pass data via variables, not hardcoded values, so tasks stay reusable:
prefer
"text_x": "$PING_ERROR"over"text_x": "Network unreachable". - Verify type compatibility between what a variable will actually hold at runtime and what the consuming parameter expects to cast it to.
6. Complete Example
Scenario: Ping a host, branch on whether it's reachable, report the result — declaring, populating, and consuming a variable end-to-end.
{
"Start": [
{
"id": "0",
"variables": [
{"variableName": "$PING_ERROR", "variableValue": "", "is_kpi": false},
{"variableName": "$NETWORK_STATUS", "variableValue": "unknown", "is_kpi": false}
]
}
],
"CmdStage": [
{"id": "1", "cmd_text": "ping -c 1 8.8.8.8", "cmd_error_output": "$PING_ERROR"}
],
"CompareText": [
{"id": "2", "text_x": "$PING_ERROR", "text_y": "unreachable", "compare_type": 2}
],
"TextReport": [
{"id": "3", "texte": "Network unreachable: $PING_ERROR"},
{"id": "4", "texte": "Network available - Status: $NETWORK_STATUS"}
],
"End": [{"id": "100"}],
"Links": [
{"from": "0", "to": "1"},
{"from": "1", "to": "2"},
{"from": "2", "true": "3", "false": "4"},
{"from": "3", "to": "100"},
{"from": "4", "to": "100"}
]
}
Execution trace:
- START declares
$PING_ERROR="",$NETWORK_STATUS="unknown". CmdStage(id:1) runs the ping; its error output is registered into$PING_ERROR.CompareText(id:2) resolves$PING_ERRORfrom the dictionary and compares it against"unreachable".- Branch: TRUE →
TextReport(id:3) resolves and displays$PING_ERROR. FALSE →TextReport(id:4) resolves and displays$NETWORK_STATUS. - Workflow reaches END; the dictionary's final state persists but is discarded once the workflow run ends.
7. Workflow Graph Structure
A workflow is a single JSON object representing a directed graph:
Start— exactly one entry point, always the first node reached. Also where variables are declared (see Section 2).- Task-type keys (e.g.
"SendSMS","CompareNumber") — zero or more of each. Every array entry is one instance of that task used in this workflow, with its own unique"id". End— one or more exit points. A workflow is NOT limited to a single End node: different branches commonly terminate at different End nodes (each with its ownid), so each path can end independently.Links— wires every node to whichever node(s) run next, byid.
Link Types
1. Sequential link — after a Normal task, exactly one next node:
Meaning: once node"0" finishes, node "1" runs next.
2. Conditional link — after a Condition-type task (e.g. CompareNumber,
IsCharging, FileExists), exactly two possible next nodes:
"2"'s boolean result decides whether "3" or "4" runs
next. A condition node's link never has a plain "to" — only "true"/"false".
3. Connector link — after a Connector-type task (e.g. Iterate,
ForEachElement), exactly one next node — same shape as a sequential link:
"5" is a connector task — it runs its own embedded
sub-workflow (see Nested Sub-Workflows below) some number of times, and
only once that's finished does node "6" run next. From the outer graph's
point of view a connector task is indistinguishable from a Normal task; all
of its repetition is invisible to Links.
Nested Sub-Workflows (Connector Tasks)
A connector task is a task whose own JSON entry contains a nested
sub-workflow, run some number of times as part of executing that one task.
This is AndroMate's native loop mechanism — an alternative to the manual
technique below of wiring a Links entry back to an earlier task id.
{
"Iterate": [
{
"id": "5",
"iteration_variable_input": "$i",
"start_index": 1,
"end_index": 3,
"do": {
"StartSubTask": [{"id": "sub-0"}],
"TextReport": [{"id": "sub-1", "texte": "i = $i"}],
"End": [{"id": "sub-100"}],
"Links": [
{"from": "sub-0", "to": "sub-1"},
{"from": "sub-1", "to": "sub-100"}
]
}
}
]
}
- The sub-workflow lives under a task-specific key (
"do"for bothIterateandForEachElement) and is structured like a workflow body, but its entry point isStartSubTask— notStart— and it carries only an"id"(novariables,Time_out, orexec_policy; those exist solely on the outer workflow's realStart). - It has its own
EndandLinks, scoped to its own task ids only — give them a distinct pattern (e.g. asub-prefix) so they never collide with ids in the outer graph. - Variables are shared, not scoped: the sub-workflow reads and writes the
exact same variable dictionary as the rest of the workflow (Section 4).
Nothing declared inside
"do"is private to it. - Full parameter/exception reference: see the Tasks Overview § Connector
Tasks page and the individual
Iterate/ForEachElementtask pages.
Rules
- Exactly one
Startnode (conventionallyid: "0"). - At least one
Endnode — but a workflow may declare several, e.g. one per branch outcome, so different paths exit independently. - Every
idreferenced inLinks(from/to/true/false) must exist as an actual node somewhere in the JSON (Start,End, or a task instance) with that exactid. This applies independently inside a connector task's own nestedLinkstoo — its ids just live in their own namespace, never mixed with the outer graph's. - Graph traversal: execution starts at
Start, followsLinks(branching ontrue/falsefor condition nodes, running a connector task's nested"do"sub-workflow to completion before following its"to"), executes each task it reaches, and that path stops the moment it reaches anyEndnode.
Complete Example — Multiple End Nodes
Scenario: read the battery level; if it's low, send an SMS alert and end there; otherwise just log it and end through a different exit point.
{
"Start": [
{"id": "0", "variables": [{"variableName": "$BATTERY_LEVEL", "variableValue": ""}]}
],
"GetBatteryLevel": [
{"id": "1", "value_output": "$BATTERY_LEVEL"}
],
"CompareNumber": [
{"id": "2", "num_x": "$BATTERY_LEVEL", "num_y": "20", "compare_type": 3}
],
"SendSMS": [
{"id": "3", "msisdn": "+33612345678", "message_body": "Low battery: $BATTERY_LEVEL%"}
],
"TextReport": [
{"id": "4", "texte": "Battery OK: $BATTERY_LEVEL%"}
],
"End": [
{"id": "100"},
{"id": "101"}
],
"Links": [
{"from": "0", "to": "1"},
{"from": "1", "to": "2"},
{"from": "2", "true": "3", "false": "4"},
{"from": "3", "to": "100"},
{"from": "4", "to": "101"}
]
}
Reading this graph:
Start(id"0") declares$BATTERY_LEVEL.GetBatteryLevel(id"1") reads it.CompareNumber(id"2") checks$BATTERY_LEVEL < 20(compare_type: 3=<).- TRUE (battery is low) →
SendSMS(id"3") →Endid"100". - FALSE (battery is fine) →
TextReport(id"4") →Endid"101"— a different End node than the TRUE branch. Both are valid exits of the same workflow; nothing requires every branch to converge on one End.