---
title: "Build a campaign on the canvas"
description: "Create a campaign, add and connect nodes, configure them, then save and activate — the two separate actions that put a campaign live on one page."
canonical: https://docs.afp.monster/guides/build-a-campaign
updated: 2026-08-19
pageType: guide
---

A campaign is a graph you draw on the canvas: pick the connected page it listens to, add a trigger node from the palette, connect it to a condition, and hang actions off the condition's outputs. **Save campaign** writes a new version of the graph; switching the campaign on is a separate action that validates the graph first. An event on your page reaches the campaign only after both have happened.

![The campaign canvas: the node palette on the left, the page selector and save and activate controls in the header, the graph in the middle, and a node's settings panel on the right](../../../assets/screenshots/guides/build-a-campaign/canvas-anatomy.webp)

## What you need first

- A connected Facebook page or Instagram business account. See [Connect a page](/start/connect-a-page).
- An active subscription with room for another campaign. The campaign list header shows your campaign count against your plan's allowance. See [Plans and quotas](/reference/plans-and-quotas).
- A keyword list, if the campaign matches message text against one. See [Match comments with keyword lists](/guides/keyword-lists).

## How do I create a campaign and choose its page?

1. Open **Campaigns** → **+ New automation**. An empty canvas opens.
2. Choose the connected page in the canvas header. AFP derives the platform from that page: a standalone Instagram business account produces an Instagram campaign, a Facebook page produces a Facebook campaign.
3. Name the campaign in the header.
4. Build the graph, as below.
5. Click **Save campaign**.

A campaign is bound to exactly one page, and creating it subscribes that page to its events, so there is no separate subscription step.

Changing the page later leaves the graph untouched — only the events the campaign listens to change — and a move to the other platform re-derives the campaign's platform from the new page.

## How do I add nodes and connect them?

The palette on the left shows only the node types valid for the current platform and, when you add from an existing node's handle, only the types that can legally follow it. Three ways to add one:

1. Drag a tile from the palette onto the canvas.
2. Click the **+** placeholder on a node's output handle, then pick a type — the node is created and connected in one motion.
3. Right-click the canvas and choose **Add node**.

To connect two nodes, drag from a free output handle onto the target node; the canvas validates while you drag and refuses an invalid connection on the spot. Dropping on empty canvas reopens the palette with that handle remembered, so the next type you pick arrives already connected.

Connections are labelled from the output they leave: a condition has **Match** and **No Match**, a random split one per arm, every other node a single output.

```d2 title="A campaign that replies publicly then sends a DM on a keyword match, and stops otherwise"
direction: right
trigger: trigger_comment
cond: "condition\nkeyword list" {shape: diamond}
reply: reply_comment
dm: send_dm
halt: stop
trigger -> cond
cond -> reply: Match
reply -> dm
cond -> halt: No Match
```

Adding, moving, connecting, and configuring all happen in your browser, and node positions are stored with the graph, so reopening a campaign puts every node back where you left it. Nothing reaches AFP until you save.

## Which connections does the canvas refuse?

| What you cannot connect | Why |
|---|---|
| Anything into a trigger node | A trigger starts the campaign, so it has no input |
| A node to itself | A node cannot be its own next step |
| Any connection that lets the graph reach a node it already passed | The graph runs forward only |
| A second connection into a node | A node accepts one incoming connection |
| A second connection out of one output handle | One handle carries one connection — branch with a condition or a random split |
| A trigger node to anything but a condition | The first node after a trigger is always a condition |
| A condition to another condition | Conditions cannot be chained |
| A comment action when the trigger is `trigger_dm` | A DM-triggered campaign has no comment to act on |
| `reply_comment` or `hide_comment` after `delete_comment` | The comment no longer exists at that point |

Two edits also remove connections you already drew, because the handle they came from disappears: deleting a button from a `send_dm` node drops the branch that left that button, and setting a condition to match any message drops its **No Match** branch.

## How do I configure a node?

Click the node. The settings panel opens on the right with the form for that type, and your changes are held with the rest of your unsaved work.

| Node | What you set |
|---|---|
| `trigger_comment` | Every post on the page or one specific post, and whether one person can trigger it more than once |
| `trigger_dm` | Whether one person can trigger it more than once |
| `condition` | The match type — any message, contains, exact, or a keyword list — the text or list to match, whether to reverse the keyword result, whether to wait for a reply, and filters for images, GIFs, and links |
| `send_dm` | The message type (text, image, card, or text with buttons) and its content |
| `reply_comment` / `send_comment` | The comment text; `send_comment` also needs the post to comment on |
| `delay` | How long to wait, from 1 to 31536000 seconds, and whether to continue only if the person has not replied while the campaign was waiting |

The remaining types take one setting each: a percentage per `random_split` arm (0 to 100, totalling no more than 100), the tags to apply on `add_tag` and `remove_tag`, and whether to block the author on `hide_comment` and `delete_comment` (Facebook only). `stop` has no settings. [Node types](/reference/node-types) lists every field.

A node with invalid configuration is flagged on the canvas, and **Save campaign** is disabled with "Fix errors to save" until you correct it.

## What is the difference between saving and activating?

Saving stores the graph. Activating decides whether the stored graph is allowed to run. A campaign needs both.

Each save writes a **new version** of the graph. An execution already in flight stays on the version it started on, so editing never changes an execution in progress. Only the current version's trigger starts new executions, and older versions are cleared away once no execution references them.

Where you toggle activation changes what gets checked:

- **In the canvas**, the **Active** toggle is part of your unsaved changes and takes effect when you press **Save campaign**, which asks you to confirm the first switch-on. Activation here validates the graph — at least one trigger node, an incoming connection on every other node, no node with configuration errors — and a failing graph is never sent, with its errors listed for you.
- **In the campaign list**, the toggle applies immediately and does not look at the graph. A campaign switched on there whose current version has no trigger node reads as active and produces nothing.

Saving an empty graph is allowed: the campaign keeps its name and page but has no trigger, so no event can start it.

Switching a campaign off leaves its page subscribed to events. That subscription only ends when the page's last campaign is deleted.

:::note
Saving does not activate. An active campaign runs the last version you saved, not the edits still open in your browser.
:::

## How do I find a campaign again?

Open **Campaigns**. Each row shows the campaign name, the page it is bound to, and its on/off toggle, and the header shows how many campaigns you have against your plan's allowance.

| Control | What it does |
|---|---|
| Search | Filters on the campaign name, matching any part of it. It does not search inside the graph. |
| Page filter | Narrows the list to campaigns bound to one connected page. |
| Sort | Orders by when each campaign was last updated, most recent first, and can be reversed. |
| Pagination | The list is returned a page at a time, so older campaigns are behind the pager rather than absent. |

If your account sits under a master account, the list also covers campaigns on the pages in that shared scope, not only the pages you added yourself. See [Manage profiles under one account](/guides/profiles-and-master-accounts).

## How do I rename, duplicate, or delete a campaign?

**Rename** by editing the name in the canvas header. The new name is written with your next **Save campaign**, along with the page binding and the active state — those three travel together, separately from the graph itself.

**Duplicating** is not something the canvas does for you: create a second campaign and build its graph again. 

**Delete** from the campaign row, then confirm. Deleting removes the campaign, every saved version of its graph, and its execution history, including test executions. If it was the last campaign on that page, AFP also stops subscribing to that page's events.

:::caution
Deleting a campaign deletes its executions with it, so the history you would use to work out what it did goes too. Read it first — see [Test and debug campaigns](/guides/test-and-debug-campaigns).
:::

## Why am I seeing a form instead of the canvas?

Campaigns built before the canvas existed have no saved graph. Opening one renders a graph built from its older definition, so the canvas can show any campaign; saving from the canvas writes a real graph, and the campaign behaves like every other canvas campaign from then on.

The **Form** button in the canvas header switches to that older form view. Nodes with no equivalent there — a random split, a DM with several buttons, multi-tag operations — block the switch: a dialog names them and offers to remove them from the canvas. A condition whose configuration only partly maps across produces a warning instead, and its node stays.

Nothing is written when you switch views. The active state carries across, and you have to save in the form view for anything to persist.

## Why is my campaign not saving or firing?

| Symptom | Cause | Fix |
|---|---|---|
| **Save campaign** is disabled, reading "Fix errors to save" | A node has invalid configuration | Complete the flagged node's settings panel |
| An alert says the campaign cannot be activated without a page | **Active** was switched on before a page was chosen | Choose the page in the canvas header |
| Saving with **Active** on lists graph errors and writes nothing | No trigger node, or a node with no incoming connection | Connect every node back to the trigger, then save |
| Creating the campaign is refused with a subscription error | The subscription is inactive or expired, or the plan's campaign allowance is used up | Renew the subscription, or delete a campaign you no longer run |
| The campaign is in the list but its canvas is empty | The campaign row was created and its first graph save did not go through | Rebuild the nodes and save again |
| The campaign is switched on and nothing happens | The current version has no trigger node | Add the trigger, save, then confirm the toggle is still on |
| One of two browser tabs fails to save | Versions are numbered per campaign, so simultaneous saves collide and one of them loses | Reload the canvas, check which edits survived, and save again |
| A campaign that used to open now returns "not found" | The page it is bound to left your scope, for example when a master-account link was removed | Restore the link, or rebuild the campaign on a page you own |

## Related

- [Route comments to a DM](/guides/route-comments-to-dm) — the four-node pattern most campaigns start from.
- [Node types](/reference/node-types) — every node and every field it takes.
- [Test and debug campaigns](/guides/test-and-debug-campaigns) — run a test execution and read the path it took.
- [Campaign graph schema](/reference/campaign-graph-schema) — how nodes, edges, and versions fit together.
- [Send your first auto-reply](/start/first-auto-reply) — the shortest version of this page.
