Skip to content

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:

{"NtpSync": [{"id": "1", "ntp_offset_output": "$TIME_OFFSET"}]}

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}):

// Before: {"num_x": "$TIME_OFFSET", "num_y": "200"}
// After:  {"num_x": "125",          "num_y": "200"}

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 Start and 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 a Double field) → 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

  1. Declare all variables upfront in the START node — a variable used before it is declared or produced causes an execution error.
  2. Use descriptive names: $PING_ERROR, $NETWORK_STATUS — not $x, $var1.
  3. Initialize with type-appropriate defaults: "" for String, "0.0" for Double, "{}" for JSON.
  4. Pass data via variables, not hardcoded values, so tasks stay reusable: prefer "text_x": "$PING_ERROR" over "text_x": "Network unreachable".
  5. 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:

  1. START declares $PING_ERROR="", $NETWORK_STATUS="unknown".
  2. CmdStage (id:1) runs the ping; its error output is registered into $PING_ERROR.
  3. CompareText (id:2) resolves $PING_ERROR from the dictionary and compares it against "unreachable".
  4. Branch: TRUETextReport (id:3) resolves and displays $PING_ERROR. FALSETextReport (id:4) resolves and displays $NETWORK_STATUS.
  5. 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".
  • Endone 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 own id), so each path can end independently.
  • Links — wires every node to whichever node(s) run next, by id.

1. Sequential link — after a Normal task, exactly one next node:

{"from": "0", "to": "1"}
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:

{"from": "2", "true": "3", "false": "4"}
Meaning: node "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:

{"from": "5", "to": "6"}
Meaning: node "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 both Iterate and ForEachElement) and is structured like a workflow body, but its entry point is StartSubTask — not Start — and it carries only an "id" (no variables, Time_out, or exec_policy; those exist solely on the outer workflow's real Start).
  • It has its own End and Links, scoped to its own task ids only — give them a distinct pattern (e.g. a sub- 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 / ForEachElement task pages.

Rules

  1. Exactly one Start node (conventionally id: "0").
  2. At least one End node — but a workflow may declare several, e.g. one per branch outcome, so different paths exit independently.
  3. Every id referenced in Links (from/to/true/false) must exist as an actual node somewhere in the JSON (Start, End, or a task instance) with that exact id. This applies independently inside a connector task's own nested Links too — its ids just live in their own namespace, never mixed with the outer graph's.
  4. Graph traversal: execution starts at Start, follows Links (branching on true/false for 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 any End node.

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:

  1. Start (id "0") declares $BATTERY_LEVEL.
  2. GetBatteryLevel (id "1") reads it.
  3. CompareNumber (id "2") checks $BATTERY_LEVEL < 20 (compare_type: 3 = <).
  4. TRUE (battery is low) → SendSMS (id "3") → End id "100".
  5. FALSE (battery is fine) → TextReport (id "4") → End id "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.