---
title: "Keyword matching rules"
description: "How a condition node compares a comment or DM against a keyword list or configured text, what an empty list matches, and which branch each result takes."
canonical: https://docs.afp.monster/reference/keyword-matching
updated: 2026-08-19
pageType: reference
---

A condition node compares the text of the message that reached it against the words configured on that node, then sends the execution out of **Match** (output 0) or **No Match** (output 1). With the **Keyword list** match type, a message matches when it contains at least one keyword from the list. Reverse matching inverts that result, and a list with no keywords — or one that has been deleted — matches every message.

## Summary

| Match type | Settings it reads | A message matches when |
|---|---|---|
| `any` | none | Always. The node keeps one output and every message follows it |
| `contains` | `text` | The message contains the configured text |
| `exact` | `text` | The message is the configured text and nothing else |
| `keyword_list` | `keyword_list_id`, `reverse_keywords` | The message contains at least one keyword from the list, or none of them when reverse matching is on |

## The matching algorithm

1. The condition takes the text of the message that started the execution — the comment, or the direct message.
2. A condition set to wait for a reply parks the execution instead of comparing anything, and compares the text of the reply when it arrives. A pressed button counts as text: the button's label is what the condition compares.
3. The match type decides the comparison. `keyword_list` loads the saved list by its identifier and tests the message against that list's keywords.
4. Reverse matching inverts the result of a `keyword_list` comparison, so a message containing none of the keywords matches.
5. A keyword list with no keywords in it, and a reference to a list that no longer exists, both produce a match without comparing anything.
6. The result picks the output. Match is output 0, No Match is output 1, and the execution continues along whichever one carries a connection. An output with no connection ends the execution at the condition.
7. Media filters on the same node — images, GIFs, and links — are separate settings and take no part in the keyword comparison, though a message that fails one still leaves by **No Match**.

```d2 title="How a condition node picks the text to match, applies the match type, and chooses its output"
direction: down
arrives: Message reaches the condition
waits: Waiting for a reply? {shape: diamond}
parks: Execution parks until a reply arrives
replyText: Take the reply text
triggerText: Take the triggering message text
matchType: Match type {shape: diamond}
matched: "Match, output 0"
nodeText: "Compare against the node's text"
listWords: "Compare against the list's keywords"
reverse: Reverse matching on? {shape: diamond}
invert: Invert the result
keep: Keep the result
decide: Matched? {shape: diamond}
unmatched: "No Match, output 1"
arrives -> waits
waits -> parks: yes
parks -> replyText
waits -> triggerText: no
replyText -> matchType
triggerText -> matchType
matchType -> matched: any
matchType -> nodeText: contains or exact
matchType -> listWords: keyword_list
listWords -> reverse
reverse -> invert: yes
reverse -> keep: no
nodeText -> decide
invert -> decide
keep -> decide
decide -> matched: yes
decide -> unmatched: no
```

Keyword settings live on the condition node. A trigger node decides which events start a campaign and holds no keyword configuration at all, so a campaign filters text only once its execution reaches a condition. [Node types](/reference/node-types) lists the configuration of both.

## `any`

- **Reads:** nothing
- **Outputs:** 1

Every message matches, with no comparison performed. Switching a condition to `any` drops the node's second output and removes any connection leaving it, so the No Match branch cannot exist while this match type is selected.

## `contains`

- **Reads:** `text`
- **Outputs:** 2

The message matches when the configured text appears inside it. `text` holds one string rather than a set of alternatives — use a keyword list when you need several.

## `exact`

- **Reads:** `text`
- **Outputs:** 2

The message matches when it is the configured text and nothing else. 

## `keyword_list`

- **Reads:** `keyword_list_id`, `reverse_keywords`
- **Outputs:** 2

The message is compared against a saved keyword list, chosen from the node's dropdown, and matches when it contains at least one of that list's keywords. The list is read by its identifier at match time, so editing the list changes what this node matches without re-saving the campaign. [Match comments with keyword lists](/guides/keyword-lists) covers creating a list and attaching it.

```json
{
  "type": "condition",
  "config": {
    "match_type": "keyword_list",
    "keyword_list_id": 41,
    "reverse_keywords": false,
    "wait_for_reply": false,
    "filter_images": false,
    "filter_gifs": false,
    "filter_links": false
  }
}
```

Keywords are cleaned when the list is saved, not when a message arrives: leading and trailing spaces are trimmed, blank entries are dropped, and repeated entries collapse to one. A list submitted with no keywords is refused.

One list can back any number of condition nodes, across campaigns and pages, because the node stores the list's identifier rather than a copy of its words.

## Reverse matching

`reverse_keywords` applies to the `keyword_list` match type and inverts the outcome of the comparison. **Match** then carries the messages that contain none of the keywords, and **No Match** carries the ones that contain at least one. Nothing else about the comparison changes.

## An empty or deleted keyword list

| Situation | What the condition does |
|---|---|
| The list has no keywords in it | Matches every message |
| The list the node points at has been deleted | Matches every message |

A condition that suddenly responds to everything usually has one of those two causes. A condition node's reference to a list lives inside the campaign's saved graph, which is why a list can disappear from under a node that still points at it — [Match comments with keyword lists](/guides/keyword-lists) covers what deletion does and does not refuse.

## Worked examples

| Comment text | Condition setting | Reverse matching | Branch taken |
|---|---|---|---|
| `How much is shipping?` | Keyword list holding `shipping` and `price` | off | Match |
| `How much is shipping?` | Keyword list holding `shipping` and `price` | on | No Match |
| `Beautiful, congratulations` | Keyword list holding `shipping` and `price` | off | No Match |
| `Beautiful, congratulations` | Keyword list holding `shipping` and `price` | on | Match |
| `Beautiful, congratulations` | Keyword list with no keywords in it | off | Match |
| `Beautiful, congratulations` | Keyword list that has been deleted | off | Match |
| A sticker with no text, which arrives as `{{sticker}}` | Keyword list holding `shipping` and `price` | off | No Match |
| A sticker with no text, which arrives as `{{sticker}}` | Keyword list holding `{{sticker}}` | off | Match |
| `How much is shipping?` | `contains`, with `text` set to `shipping` | not applicable | Match |
| `Anything at all` | `any` | not applicable | Match, on the node's single output |

## Case, whole words, punctuation, and emoji

A message matches by containing a keyword, but how the two strings are prepared before that test is not confirmed. Treat each statement below as the expected behaviour rather than a guarantee, and test the ones your campaign depends on:

- Letter case is ignored, so a comment reading `Shipping` matches a keyword of `shipping`. 
- A keyword matches inside a longer word, so `ship` matches a comment reading `shipping`. 
- Punctuation beside a keyword does not prevent a match, so `shipping?` matches a keyword of `shipping`. 
- Emoji are compared as ordinary characters, so a keyword holding one emoji matches a comment containing that emoji. 

What a keyword list stores is settled, whatever the comparison turns out to do: spaces around an entry are trimmed on save, so a keyword can never carry leading or trailing whitespace into a comparison.

Until the rest is confirmed, settle each keyword with a test execution from the canvas: run the campaign against the exact text you expect to receive and read which output the condition took. See [Test and debug a campaign](/guides/test-and-debug-campaigns).

## Notes

- A comment carrying no text arrives with a placeholder in place of the message — `{{sticker}}` on Facebook, `{{media}}` on an Instagram reply — so a condition always has something to compare. A keyword list holding that exact string matches those comments.
- An execution parked at a condition waiting for a reply keeps the keyword set it started with. A reply arriving after you edit the list is matched against the old keywords; the next execution uses the new ones.
- A condition with the image filter turned on does not match a reply that carries no image, and routes it to **No Match** whatever the keywords say.
- Changing a condition's match type or text on the canvas alters the campaign in your browser only. A live execution compares against the saved version of the graph it started on, so the change applies to new executions once you save.
- A condition never connects to another condition, so a branch tests message text once. Two tests on one path need two branches, or a keyword list that covers both sets of words.

## Related

- [Match comments with keyword lists](/guides/keyword-lists) — creating a list, attaching it to a node, and what happens when you edit or delete one.
- [Node types](/reference/node-types) — the condition node's full configuration, alongside every other node.
- [Campaign graph schema](/reference/campaign-graph-schema) — where a condition's configuration sits in a saved graph.
- [Trigger events](/reference/trigger-events) — the events that produce the text a condition compares.
- [Contact fields](/reference/contact-fields) — what an execution can change about a contact after it leaves the **Match** output.
