---
title: "Node types"
description: "Every node type you can add from the campaign canvas, with its category, outputs, required configuration, and what it does when an execution reaches it."
canonical: https://docs.afp.monster/reference/node-types
updated: 2026-08-19
pageType: reference
---

Thirteen node types can be added to a campaign from the canvas palette: two triggers, four logic nodes, one message node, four comment actions, and two contact actions. Each type carries its own configuration keys and a fixed number of outputs, and those two things decide what an execution does at that node and where it goes next. Node types that exist in the runtime but have no palette tile are listed in the [campaign graph schema](/reference/campaign-graph-schema).

## Summary

| Node type | Category | Outputs | Required config | What it does |
|---|---|---|---|---|
| `trigger_comment` | trigger | 1 | `post_id` when `post_scope` is `specific` | Starts the campaign when someone comments on the page |
| `trigger_dm` | trigger | 1 | none | Starts the campaign when someone sends the page a direct message |
| `condition` | logic | 2 (Match / No Match) | `match_type`, plus `text` or `keyword_list_id` for the match types that read them | Matches the message text and picks a branch, or parks the execution until a reply arrives |
| `send_dm` | message | 1, plus one per `postback` button | `message_type` and the keys that type reads | Sends a direct message: text, image, card, or text with buttons |
| `reply_comment` | comments | 1 | `text` | Replies publicly under the comment that started the execution |
| `send_comment` | comments | 1 | `text`, `post_id` | Posts a new comment on a named post |
| `hide_comment` | comments | 1 | none | Hides the comment that started the execution |
| `delete_comment` | comments | 1 | none | Deletes the comment that started the execution |
| `delay` | logic | 1 | none | Holds the execution and continues it later |
| `random_split` | logic | one per arm | `paths` | Sends each execution down one arm, chosen at random |
| `add_tag` | contact | 1 | `tag_ids` | Adds tags to the contact |
| `remove_tag` | contact | 1 | `tag_ids` | Removes tags from the contact |
| `stop` | logic | 0 | none | Ends the execution |

## `trigger_comment`

- **Category:** trigger
- **Outputs:** 1, and it connects only to a `condition`
- **Required config:** `post_id` when `post_scope` is `specific`

| Key | Type | What it sets |
|---|---|---|
| `post_scope` | string | `all` or `specific`. An absent key counts as `all`. |
| `post_id` | string | The post a `specific` scope listens on. The settings panel offers the connected page's posts. |
| `unique_per_user` | boolean | Turns on the once-per-person restriction described below. |

Starts an execution when a comment arrives on the connected page. With `post_scope` set to `specific`, only comments on the post named by `post_id` start the campaign; every other comment on the page is ignored.

`unique_per_user` restricts the campaign to a single execution per person. 

```json
{
  "post_scope": "specific",
  "post_id": "104857392017465_998234511",
  "unique_per_user": true
}
```

## `trigger_dm`

- **Category:** trigger
- **Outputs:** 1, and it connects only to a `condition`
- **Required config:** none

| Key | Type | What it sets |
|---|---|---|
| `unique_per_user` | boolean | Turns on the once-per-person restriction described below. |

Starts an execution when someone sends the connected page a direct message. There is no `post_scope` key, because a direct message is not attached to a post. Comment actions are unavailable in a campaign whose trigger is `trigger_dm`.

`unique_per_user` is the same setting as on `trigger_comment`, and restricts the campaign to a single execution per person. 

```json
{
  "unique_per_user": false
}
```

## `condition`

- **Category:** logic
- **Outputs:** 2 — output 0 is Match, output 1 is No Match
- **Required config:** `match_type`, plus `text` or `keyword_list_id` for the match types that read them

| Key | Type | What it sets |
|---|---|---|
| `match_type` | string | `any`, `contains`, `exact`, or `keyword_list`. Decides the comparison and which keys below are read. |
| `text` | string | The text compared by `contains` and `exact`. |
| `keyword_list_id` | number | The saved keyword list compared by `keyword_list`. |
| `reverse_keywords` | boolean | Inverts the outcome of a `keyword_list` comparison. |
| `wait_for_reply` | boolean | Parks the execution at this node instead of evaluating the message that reached it. |
| `filter_images` | boolean | Media filter on the incoming message. |
| `filter_gifs` | boolean | Media filter on the incoming message. |
| `filter_links` | boolean | Media filter on the incoming message. |

Evaluates the message and sends the execution down the Match or the No Match output. [Keyword matching rules](/reference/keyword-matching) covers what each match type compares and which output it produces.

`wait_for_reply` parks the execution at this node instead of evaluating straight away, and it stays there until the person replies. [Execution states](/reference/execution-states) covers how long a parked execution survives and what wakes it up.

The media filters are separate settings on the same node and take no part in the keyword comparison. [Match comments with keyword lists](/guides/keyword-lists) covers building the list that `keyword_list_id` points at.

```json
{
  "match_type": "keyword_list",
  "keyword_list_id": 41,
  "reverse_keywords": false,
  "wait_for_reply": true
}
```

## `send_dm`

- **Category:** message
- **Outputs:** 1, plus one extra output for every `postback` button
- **Required config:** `message_type` and the keys that type reads

| Key | Type | What it sets |
|---|---|---|
| `message_type` | string | `text`, `image`, `card`, or `text_with_buttons`. Decides which keys below are read. |
| `text` | string | The message text. |
| `image_url` | string | The image for an `image` message, and the picture on a `card`. |
| `title` | string | The heading on a `card`. |
| `subtitle` | string | The second line on a `card`. |
| `buttons` | array | Buttons on a card or a button message. Each entry carries an `id`, a `type` of `web_url` or `postback`, a `title`, and a `url` for a `web_url` button. |
| `buttons_text` | string | One button label per line, for a `text_with_buttons` message. Each line becomes a `postback` button whose payload is the label. |

Sends one direct message to the person the execution is running for. `message_type` selects which keys are read: `text` for a plain message, `image_url` for an image, `title`, `subtitle`, and `image_url` for a card, and `buttons_text` for text with buttons.

Sender-name variables in `text`, `title`, and `subtitle` are substituted before the message is sent.

A `postback` button adds its own output to the node, so an execution can continue down a different branch depending on which button the person presses.

A node with nothing configured sends nothing, and the execution carries on to the next node.

```json
{
  "message_type": "text_with_buttons",
  "text": "Thanks for commenting. Which size do you want?",
  "buttons_text": "Small\nMedium\nLarge"
}
```

## `reply_comment`

- **Category:** comments
- **Outputs:** 1
- **Required config:** `text`

| Key | Type | What it sets |
|---|---|---|
| `text` | string | The reply posted under the triggering comment. |

Posts a public reply under the comment that started the execution. Sender-name variables in `text` are substituted before the reply is posted.

The node is unavailable in a campaign triggered by `trigger_dm`, and the canvas refuses to place it downstream of a `delete_comment`.

```json
{
  "text": "Sent you the price list in a DM."
}
```

## `send_comment`

- **Category:** comments
- **Outputs:** 1
- **Required config:** `text`, `post_id`

| Key | Type | What it sets |
|---|---|---|
| `text` | string | The comment posted on the named post. |
| `post_id` | string | The post the comment is published on. |

Posts a new top-level comment on the post named by `post_id`. There is no fallback to the post the execution came from: without `post_id` the node does nothing and the execution carries on to the next node.

The node is unavailable in a campaign triggered by `trigger_dm`.

```json
{
  "text": "Restocked today — sizes S to XL are back.",
  "post_id": "104857392017465_998234511"
}
```

## `hide_comment`

- **Category:** comments
- **Outputs:** 1
- **Required config:** none

| Key | Type | What it sets |
|---|---|---|
| `also_block_user` | boolean | Blocks the comment's author in the same step. Facebook only. |

Hides the comment that started the execution.

`also_block_user` blocks the comment's author in the same step and works on Facebook only — the option is disabled on an Instagram campaign.

## `delete_comment`

- **Category:** comments
- **Outputs:** 1
- **Required config:** none

| Key | Type | What it sets |
|---|---|---|
| `also_block_user` | boolean | Blocks the comment's author in the same step. Facebook only. |

Deletes the comment that started the execution. `also_block_user` works on Facebook only, the same as on `hide_comment`.

Downstream of this node the canvas refuses `reply_comment` and `hide_comment`, because the comment those nodes would act on no longer exists.

## `delay`

- **Category:** logic
- **Outputs:** 1
- **Required config:** none

| Key | Type | What it sets |
|---|---|---|
| `delay_seconds` | number | How long the execution waits, from 1 to 31536000 seconds. |
| `only_if_no_reply` | boolean | Cancels the rest of the execution if the person replied while the wait was running. |

Holds the execution and schedules it to continue later. `delay_seconds` is clamped to between 1 and 31536000 seconds (365 days), and a value outside that range is clipped without an error. A node with no value configured waits 60 seconds.

`only_if_no_reply` is checked when the wait elapses: if the person replied at any point during it, the execution completes at the delay instead of continuing down the branch.

```json
{
  "delay_seconds": 3600,
  "only_if_no_reply": true
}
```

## `random_split`

- **Category:** logic
- **Outputs:** one per entry in `paths`
- **Required config:** `paths`

| Key | Type | What it sets |
|---|---|---|
| `paths` | array | One `{ "percent": number }` entry per output. Each percentage is 0 to 100 and the total cannot exceed 100. |

Splits traffic between branches: each execution follows one arm, chosen at random. Set one percentage per arm in the canvas; each is 0 to 100 and the total cannot exceed 100.

Treat the percentages as the split you are asking for rather than a guaranteed ratio, and confirm the actual distribution in the execution history before relying on it for anything that matters. 

With no percentages configured the node falls back to two equal arms.

The chosen arm decides which output the execution follows. If that output has no connection, the execution completes at this node.

```json
{
  "paths": [{ "percent": 70 }, { "percent": 30 }]
}
```

## `add_tag`

- **Category:** contact
- **Outputs:** 1
- **Required config:** `tag_ids`

| Key | Type | What it sets |
|---|---|---|
| `tag_ids` | array | The numeric identifiers of the tags to add. |

Adds the named [tags](/guides/tags) to the contact the execution is running for, alongside the tags that contact already carries.

When the execution has no contact behind it, the node changes nothing and the execution carries on to the next node.

```json
{
  "tag_ids": [12, 19]
}
```

## `remove_tag`

- **Category:** contact
- **Outputs:** 1
- **Required config:** `tag_ids`

| Key | Type | What it sets |
|---|---|---|
| `tag_ids` | array | The numeric identifiers of the tags to remove. |

Removes the named tags from the contact the execution is running for, and leaves that contact's other tags in place. [Contact fields](/reference/contact-fields) lists the rest of the contact record.

When the execution has no contact behind it, the node changes nothing and the execution carries on to the next node.

```json
{
  "tag_ids": [19]
}
```

## `stop`

- **Category:** logic
- **Outputs:** 0
- **Required config:** none

Ends the execution as soon as it is reached, and records that the flow was stopped. The node has no output, so nothing can be connected after it.

## Node types outside the palette

Eight further node types exist in the runtime, carry no palette tile and no settings panel, and never appear in a campaign built on the canvas. [Campaign graph schema](/reference/campaign-graph-schema) names them and says what an execution does at each. Treat them as runtime-only rather than as features.

## Notes

- Outputs are numbered from 0. A `condition` uses 0 for Match and 1 for No Match, a `random_split` uses one number per arm in configured order, and every other node continues from 0.
- An execution follows the connection leaving the output the node selected. When that output carries no connection, the execution completes at that node.
- A node whose action raises an error records the error against the execution and does not advance, so the execution goes no further than that node.
- An execution that arrives at a node it has already visited ends with a cycle error naming that node. Every node runs at most once per execution.
- A node type the runtime does not recognise is passed over with a warning, and the execution continues from output 0.
- A comment action that has no comment to act on does nothing, and the execution continues.
- Palette tiles are filtered by the campaign's platform, so the palette on an Instagram campaign can offer fewer node types than the palette on a Facebook one.
- Which node types may follow which is enforced while a connection is dragged, not when the campaign is saved. [Campaign graph schema](/reference/campaign-graph-schema) lists those constraints.
- In a test execution, `add_tag` and `remove_tag` record what they would have done and leave the contact untouched, and a `delay` ends the test run instead of scheduling a resume.

## Related

- [Campaign graph schema](/reference/campaign-graph-schema) — how nodes, connections, and configuration are stored
- [Build a campaign](/guides/build-a-campaign) — adding and connecting these nodes on the canvas
- [Keyword matching rules](/reference/keyword-matching) — what each `condition` match type compares
- [Execution states](/reference/execution-states) — what an execution looks like as it passes through these nodes
- [Contact fields](/reference/contact-fields) — the contact record the tag nodes write to
