Skip to main content

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

HomeLives forVisible toTypical use
Flow variable (scope: flow)One runThat run onlyCounters, loop state, OCR results
Instance variable (scope: global)Forever, across restartsEvery flow on one instance"Which account is logged in on this instance"
Runtime MemoryPer-entry TTL you chooseDepends on memory scope (below)"Don't touch this target again for 15 minutes"
Flow Input ParameterOne run, supplied at startThat 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:

  1. Every declared variable is set to null.
  2. Declaration defaultValue is applied where present.
  3. The flow's initialValues are applied.
  4. Each value is checked against its declared type.
  5. 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:

OperationEffect
setEvaluate an expression and store the result
clearReset the variable to its declared default
deleteRemove the variable from the context
logWrite 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.

Counter pattern

The most useful two-node idiom in the product:

  1. A Variable node: vars.items_done = {{items_done}} + 1
  2. 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".

Instance variables are sticky

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:

ScopeSurvives run endSurvives app restartShared across instances
runNoNoNo
sessionYesNo — one app launchNo — private per instance
persistYesYesNo — private per instance
sharedYesYesYes — the only cross-instance scope
  • run lives on the run's own execution context. It disappears with the run.
  • session means "this app launch". A fresh launch starts empty.
  • persist is SQLite-backed and restart-safe.
  • shared is 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 shared

If 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

OperationWhat it doesNeeds a key
rememberAppend an entry (blank key auto-generates one)No
forgetRemove one entry by keyYes
clearEmpty the whole memoryNo
countWrite the number of live entries into a variableNo

Choosing: a decision table

You want to…Use
Count iterations inside one runFlow variable
Pass a number in before the run startsFlow Input Parameter
Remember which account this emulator usesInstance variable
Avoid re-visiting a target for 15 minutesRuntime Memory, persist, TTL
Avoid re-visiting it only within this runRuntime Memory, run
Let all instances avoid one shared target listRuntime 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, not null. null + 1 is 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 drivemax_per_trip, not n.
  • 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.
  • persist and shared memory operations hit SQLite. They are fast, but they are I/O — avoid putting a remember inside a tight inner loop that runs hundreds of times per second.
  • count scans 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 run scope is held on the execution context and released when the run finalizes.
  • session scope 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

MistakeSymptomFix
Counter declared without a defaultArithmetic produces nothingDefault it to 0
Non-nullable variable bound to an OCR resultRun rejects with TYPE_MISMATCHDeclare it number? / string?
Using shared memory for per-device stateInstances overwrite each otherUse persist
Expecting session memory to survive a restartEmpty after relaunchUse persist
Resetting a trip counter at loop topIt never reaches its limitReset at the trip boundary
Assuming an Instance variable starts cleanStale value from days agoClear 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.