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
| Setting | Type | Range | Notes |
|---|---|---|---|
| Memory | string | 1–100 chars | Required. The store to search |
| Expression | string | ≤ 1000 chars | The condition, referencing {{item.<field>}} |
| Conditions | list | 1–50 rows | Legacy no-code builder |
| Logic | choice | and / or | How 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.
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
| Port | Taken when | Required |
|---|---|---|
| matched | Some live entry satisfied the condition | No — lenient |
| notMatched | No entry satisfied it | No — lenient |
| Fatal | Invalid configuration | No — 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
persistorsharedmemory involves SQLite;runscope 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
| Mistake | What happens | Fix |
|---|---|---|
| Remembering before matching | Always matches its own entry | Match first |
| Field name typo | Never matches | Keep names identical to the write |
| Memory name typo | Empty memory, permanent notMatched | Check the name |
| Exact equality on OCR values | Misses near-identical entries | Use tolerance with abs() |
| Both ports unwired | Path ends silently | Wire both |
| Expecting a scope selector | There is none | The 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 ||.
Related nodes
- Runtime Memory — writes the entries this node reads
- Variable — prepares the candidate values
- Read Text — usually supplies the identity
- If — for comparing plain variables