Skip to main content

Memory Match

Asks whether a candidate matches anything already remembered, and branches on the answer.

Purpose

Tests a candidate against every live entry in a named memory. If any entry satisfies the condition, the node routes matched; otherwise notMatched.

The condition is a full expression, so matching can use tolerance, ranges, and string operations — not just equality.

When to use it

  • Cooldown guards: "have I handled this target recently?"
  • Deduplication: "is this item already in my processed list?"
  • Proximity checks: "is there a remembered entry near this position?"

When not to use it

  • To read one known key — that is a lookup, not a match.
  • To compare two variables — use If.
  • Before anything has been remembered — the memory will simply be empty and every candidate routes notMatched.

Requirements

A memory name, and a condition expression. The memory must have been written by a Runtime Memory node.

How it runs

Expired entries are not considered — the TTL you set on the write is what makes the guard self-clearing.

Settings

SettingTypeRangeNotes
Memorystring1–100 charsRequired. The store to search
Expressionstring≤ 1000 charsThe condition, referencing {{item.<field>}}
Conditionslist1–50 rowsLegacy no-code builder
Logicchoiceand / orHow legacy rows combine

Referencing the entry under test

Inside the expression, the entry being tested is exposed as item:

{{item.x_coordinate}}
{{item.status}}

Your candidate values come from ordinary flow variables.

Tolerance matching — the reason this node exists

Exact equality is almost useless against real screen data. The expression engine lets you express closeness:

(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?"

This is what makes the pattern work when OCR reads coordinates that drift by a pixel, or when a target's reported position shifts slightly between frames.

No scope setting

Memory Match reads across scopes — it does not take a scope parameter. Older saved nodes may carry a scope field; it is tolerated and ignored.

Match the memory name carefully

Since there is no scope selector on this node, the memory name is what ties it to your writes. A typo means an empty memory and a permanent notMatched.

Ports

PortTaken whenRequired
matchedSome live entry satisfied the conditionNo — lenient
notMatchedNo entry satisfied itNo — lenient
FatalInvalid configurationNo — never followed

Both branches are optional. Leaving one unwired ends that path gracefully, which is a legitimate "give up here" choice — but usually you want both.

Basic example

Has this item been processed?

  • Memory: processed_items
  • Expression: {{item.id}} == {{current_id}}

matched → skip; notMatched → process it.

Realistic example — the cooldown guard in full

Note the ordering: match first, then remember. Remembering before matching would always match your own entry.

Note also the Failure path from the read — if you cannot identify the target, processing it anyway is usually safer than skipping work forever.

Advanced example — a status filter

Entries can carry any fields, so the match can express business rules:

{{item.account}} == {{current_account}} && {{item.status}} == "pending"

matched means "this account already has pending work" — so skip queuing more.

Best practices

  • Always match before you remember. Otherwise the candidate matches itself.
  • Use tolerance, not equality, for anything derived from the screen.
  • Keep field names identical between the Runtime Memory write and this expression.
  • Wire both ports. Lenient does not mean you should leave them open.
  • Handle the "could not identify" case separately — an unreadable candidate is not the same as an unmatched one.
  • Test with an empty memory first. Everything should route notMatched.

Performance notes

  • The expression is evaluated once per live entry. A memory with 500 entries means 500 evaluations per call.
  • Keep memories small by setting realistic TTLs — expired entries are not evaluated, so TTL is your main cost control.
  • Reading persist or shared memory involves SQLite; run scope is in-memory.
  • Placing a Memory Match inside a tight loop over a large memory is the one way to make this node expensive.

Memory notes

  • The node reads entries; it never writes. It cannot grow a memory.
  • Live entries are materialised for evaluation, so a very large memory has a real working-set cost at match time.

Common mistakes

MistakeWhat happensFix
Remembering before matchingAlways matches its own entryMatch first
Field name typoNever matchesKeep names identical to the write
Memory name typoEmpty memory, permanent notMatchedCheck the name
Exact equality on OCR valuesMisses near-identical entriesUse tolerance with abs()
Both ports unwiredPath ends silentlyWire both
Expecting a scope selectorThere is noneThe memory name is the link

Troubleshooting

It never matches. Three usual causes: the memory name differs from the write, the field names differ, or the entries have already expired. Confirm the write's TTL and scope first.

It always matches. The node is running after the remember, so the candidate matches itself. Reorder.

It matches the wrong entries. The tolerance is too loose. Tighten the comparison.

The node routes Fatal. The expression is invalid or references something unusable.

FAQ

Does it search a specific scope? No — it reads across scopes. The memory name identifies the store.

Does it tell me which entry matched? The current model routes matched/notMatched only. Store what you need in variables before the check.

Do expired entries count? No. Only live entries are evaluated.

Can I use AND and OR together? Yes, in the expression — && and ||.

  • Runtime Memory — writes the entries this node reads
  • Variable — prepares the candidate values
  • Read Text — usually supplies the identity
  • If — for comparing plain variables