Variables, Inputs and Memory
Microbe Studio gives you four different places to keep a value, and they differ in exactly one way: how long the value lives, and who can see it. Choosing the wrong one is the most common cause of "my counter reset" and "my two instances are fighting over the same value".
This page maps all four.
Purpose
Use this page to decide, for any piece of data:
- Should it be a variable or a memory entry?
- Should it survive the run? The app restart? Should other instances see it?
- Should the operator be able to type it in before the run starts?
The four homes at a glance
| Home | Lives for | Visible to | Typical use |
|---|---|---|---|
Flow variable (scope: flow) | One run | That run only | Counters, loop state, OCR results |
Instance variable (scope: global) | Forever, across restarts | Every flow on one instance | "Which account is logged in on this instance" |
| Runtime Memory | Per-entry TTL you choose | Depends on memory scope (below) | "Don't touch this target again for 15 minutes" |
| Flow Input Parameter | One run, supplied at start | That run only | "How many items to process this trip" |
Flow variables
A flow variable is declared in the flow's Variables dialog with a path (vars.gem_farmed_trip), a type, and an optional default value.
Types
The declared types are:
string, number, boolean, point, rect, object, array, null
Each type except null also has a nullable variant written with a trailing ? — number?, string?, and so on. A nullable variable is allowed to hold null; a non-nullable one is not, and a value that violates its declaration rejects the run start with TYPE_MISMATCH.
How they are initialised
At the start of every run, in this exact order:
- Every declared variable is set to
null. - Declaration
defaultValueis applied where present. - The flow's
initialValuesare applied. - Each value is checked against its declared type.
- If any value violates its declaration, the run does not start.
This ordering means a default value always wins over null, and an initial value always wins over a default.
Reading and writing
You reference a variable in almost any text field with either syntax — both are accepted and identical:
{{gem_farmed_trip}}
${gem_farmed_trip}
You write one with the Variable node, which holds a list of assignment rows. Each row has a target and an operation:
| Operation | Effect |
|---|---|
set | Evaluate an expression and store the result |
clear | Reset the variable to its declared default |
delete | Remove the variable from the context |
log | Write the current value to the run log (no change) |
A set row may also carry an expire after (ttlSeconds, up to one year). When it expires the variable reverts to its declaration default rather than disappearing.
The most useful two-node idiom in the product:
- A Variable node:
vars.items_done = {{items_done}} + 1 - An If node:
{{items_done}} >= {{max_per_trip}}
That pair gives you "do this N times, where N is supplied per run" without a loop node, and it survives branching that a loop counter would not.
Instance variables
Setting scope: global on a declaration — labelled Instance in the UI — changes the storage completely:
- The value persists across runs and across app restarts.
- It is shared by every flow running on that one instance.
- It is isolated between instances: instance A and instance B each have their own copy.
Use it for facts about the device, not about the run. "Which account is logged in", "has this instance completed first-time setup", "what server is this instance on".
Because they survive restarts, a stale Instance variable will quietly affect runs days later. Give them a deliberate reset path — a clear row in a setup flow — rather than assuming a fresh start.
Flow Input Parameters
Mark a declaration with input and it becomes a Flow Input Parameter: still an ordinary variable inside the flow, but also surfaced for entry before a run.
- The operator fills it in on the Dashboard Run Condition form.
- Marking it required blocks the run if it is left blank.
- A Sub Flow node can bind a parent expression to a child's input, which is true argument passing.
This is what turns a flow into a reusable function:
The child declares what it needs; the parent supplies it at the call site. The child has no knowledge of its caller, so the same child works from a Queue entry, a Schedule, or three different parents.
Runtime Memory
Variables answer "what is true right now in this run". Runtime Memory answers a different question: "have I already dealt with this thing, and how recently?"
It is a multi-entry key/value store with a per-entry TTL, written by the Runtime Memory node and queried by the Memory Match node.
The four memory scopes
This is the part worth learning precisely:
| Scope | Survives run end | Survives app restart | Shared across instances |
|---|---|---|---|
run | No | No | No |
session | Yes | No — one app launch | No — private per instance |
persist | Yes | Yes | No — private per instance |
shared | Yes | Yes | Yes — the only cross-instance scope |
runlives on the run's own execution context. It disappears with the run.sessionmeans "this app launch". A fresh launch starts empty.persistis SQLite-backed and restart-safe.sharedis the single deliberate opt-in for cross-instance coordination. Every other scope namespaces entries per instance, so instance A can never read or overwrite instance B's memory.
shared is genuinely sharedIf ten instances write the same key in shared scope, they overwrite each other. Use it only when cross-instance coordination is the point — for example a global "this target is claimed" list. For anything per-device, use persist.
The cooldown-guard pattern
The canonical use of Runtime Memory: don't re-process something you handled recently.
Memory Match compares a candidate against every live entry using an expression that can reference the entry under test as {{item.<field>}}:
(abs({{current_x}} - {{item.x_coordinate}}) <= 1) && (abs({{current_y}} - {{item.y_coordinate}}) <= 1)
That reads as "is there a remembered entry within one tile of where I am now?" — tolerance matching, not exact equality, which is what real screen data needs.
Because the entry carries a TTL of 900 seconds, the guard expires by itself after 15 minutes. Nothing has to clean it up.
Memory operations
| Operation | What it does | Needs a key |
|---|---|---|
remember | Append an entry (blank key auto-generates one) | No |
forget | Remove one entry by key | Yes |
clear | Empty the whole memory | No |
count | Write the number of live entries into a variable | No |
Choosing: a decision table
| You want to… | Use |
|---|---|
| Count iterations inside one run | Flow variable |
| Pass a number in before the run starts | Flow Input Parameter |
| Remember which account this emulator uses | Instance variable |
| Avoid re-visiting a target for 15 minutes | Runtime Memory, persist, TTL |
| Avoid re-visiting it only within this run | Runtime Memory, run |
| Let all instances avoid one shared target list | Runtime Memory, shared |
Best practices
- Declare every variable you use. Undeclared references fail at start rather than mid-run, which is the failure you want.
- Give counters a default of
0, notnull.null + 1is not a number. - Use
number?for anything OCR writes. A failed read produces no value, and a non-nullable declaration will reject it. - Prefer TTL over manual cleanup. A memory entry that expires itself cannot leak.
- Name inputs after the decision they drive —
max_per_trip, notn. - Reset trip counters at the natural boundary, not at the top of the loop. In the reference automation, the counter resets when the flow returns to base — the point where "a trip" genuinely ends.
Performance notes
- Variable reads and expression evaluation are in-memory and effectively free. Do not avoid variables for performance reasons.
persistandsharedmemory operations hit SQLite. They are fast, but they are I/O — avoid putting arememberinside a tight inner loop that runs hundreds of times per second.countscans the memory's live entries. With large memories, prefer a counter variable you maintain yourself.
Memory notes
- Each session carries its own copy of all flow variables. Running one flow on 10 instances means 10 independent variable sets.
- Runtime Memory in
runscope is held on the execution context and released when the run finalizes. sessionscope is backed by the shared database under a per-launch token; the app reclaims previous launches' rows at startup, so it does not grow without bound.- Memory entries expire by absolute time, so an entry written with a 900-second TTL is still expired after a restart — the expiry is stored, not counted down in memory.
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Counter declared without a default | Arithmetic produces nothing | Default it to 0 |
| Non-nullable variable bound to an OCR result | Run rejects with TYPE_MISMATCH | Declare it number? / string? |
Using shared memory for per-device state | Instances overwrite each other | Use persist |
Expecting session memory to survive a restart | Empty after relaunch | Use persist |
| Resetting a trip counter at loop top | It never reaches its limit | Reset at the trip boundary |
| Assuming an Instance variable starts clean | Stale value from days ago | Clear it explicitly in setup |
Troubleshooting
My counter is always 1.
The Variable node is resetting it rather than incrementing. Check the expression is {{x}} + 1 and not 1.
The run refuses to start with TYPE_MISMATCH.
A default or initial value does not match its declared type. Most often a nullable OCR target declared non-nullable.
Memory Match never matches.
Check the scope on both nodes — a remember in run scope is invisible to a match reading persist. Also confirm the field names in the expression match the fields you stored.
Two instances keep skipping each other's work.
They are sharing a shared-scope memory. Switch to persist for per-device isolation.
FAQ
Is {{name}} different from ${name}?
No. Both are accepted and behave identically. Pick one and stay consistent.
Can a Sub Flow change its parent's variables? The child receives bound inputs as its own locals. Use the child's terminal status, or a shared memory scope, to communicate results back.
What happens to a variable when its TTL expires?
It reverts to the declared default value — it does not become null unless that is the default.
Do Instance variables sync between machines? No. They are local to the instance on that installation.
Related pages
- Execution Model — the context these values live in
- Flow Anatomy — where declarations are stored
- Variable node — the node that writes them
- Runtime Memory node — the memory writer
- Memory Match node — the memory reader
- Sub Flow node — argument passing