Guides/How to Build Notifications with AI Agents/Designing Journeys with AI, and the AI Node

Chapter 6

Designing Journeys with AI, and the AI Node

A journey is a multi-step flow that Courier runs on its own: send, wait, check something, branch, send again. The state lives on Courier's side, which is why a seven-day delay doesn't require a cron job or a row in your database tracking where everyone is.

how to build product notifications with AI

Last updated: September 2026

A journey is a multi-step flow that Courier runs on its own: send, wait, check something, branch, send again. The state lives on Courier's side, which is why a seven-day delay doesn't require a cron job or a row in your database tracking where everyone is.

Start with what an agent produces when you describe a flow, then look at the AI node, which puts a model inside that flow as a step.

What an agent actually generates when you describe a flow

A journey is JSON with a nodes array. Here's the prompt:

Build a welcome journey. Send the welcome email immediately, wait one day, then send an in-app reminder only if the user hasn't finished setup. Create it as a draft.

And roughly what comes back:

{
"name": "Welcome Journey",
"state": "DRAFT",
"enabled": true,
"nodes": [
{ "type": "trigger", "trigger_type": "api-invoke" },
{ "type": "send", "message": { "template": "TEMPLATE_ID" } },
{ "type": "delay", "mode": "duration", "duration": "P1D" },
{
"type": "branch",
"paths": [
{
"label": "Setup complete",
"conditions": ["data.setup_complete", "is equal", "true"],
"nodes": []
}
],
"default": {
"label": "Needs a nudge",
"nodes": [{ "type": "send", "message": { "template": "REMINDER_TEMPLATE_ID" } }]
}
},
{ "type": "exit" }
]
}

Three rules that will bite you if you hand this to an agent, all of which return a 400 rather than failing quietly:

  • Don't supply node ids. The API rejects them: client-supplied node ids are not allowed; ids are server-generated. You get the generated ids back in the response
  • A single condition is a bare atom, not a group. {"AND": [[...]]} with one entry fails with Too small: expected array to have >=2 items. Use AND or OR only when you genuinely have two or more conditions
  • The exit node must be the last node in the journey. You can't scatter exits inside branch paths; give a terminating path an empty nodes array and put one exit at the end

Then you invoke it per user, with whatever data the branches need.

You'll notice branches require a default path. Every run has to go somewhere, and Courier makes you say where rather than letting people fall off the end.

What you can ask a journey to do

Rather than memorizing node types, it's more useful to know what's askable. There are eleven, and this is what they mean in a prompt:

  • Send a message at a point in the flow
  • Wait, either for a duration or until a timestamp
  • Fetch data from your API mid-run and use it downstream
  • Branch on conditions
  • Roll events up into one payload (batch)
  • Add to a digest that goes out on the recipient's schedule
  • Call a model and use its structured output (the AI node)
  • Cancel runs when the thing they were about resolves
  • Exit

There's also a throttle node, which caps how often runs pass a point.

Cancel is the one people forget and then need. If you've started a three-message dunning sequence and the invoice gets paid, you want those runs to stop.

What the agent handles so you don't have to

Building a journey through the API takes a few ordered steps: create the journey shell, create the templates each send references, wire the graph, publish. Your agent does this.

The one thing worth knowing, because it looks like a bug when you hit it: you can't create a journey with its send nodes in a single call. Every send references a template scoped to that journey, and those templates have to exist first. If your agent tries it in one shot and gets an error, the fix is "build the templates first," not "something is broken."

Journey templates belong to their journey. You can't reference one from a regular send, and you can't import an existing workspace template into a journey.

Branching on what already happened

Here's the thing that makes journeys more than a scheduler: a branch can route on whether the previous message actually landed.

After the welcome email, wait a day. If they haven't opened it, send a push instead.

That's a real flow rather than a guess, because Courier knows the delivery status of the message the journey itself sent:

["send_status.<SEND_NODE_ID>", "was", "OPENED"]

<SEND_NODE_ID> is the server-generated id of the earlier send node, which you read back from the create or replace response. A lone condition is written as a bare atom like this; wrap it in AND or OR only once you have two or more.

One mechanic deserves calling out, because it's counterintuitive and an agent will get it wrong. The statuses are cumulative and ordered: SENT < DELIVERED < OPENED < CLICKED. So was DELIVERED is true for a message that was later opened or clicked, not only for one that stopped at delivered. UNDELIVERABLE sits outside that ordering and matches exactly.

If you want "delivered but not opened," that's was DELIVERED and was not OPENED together, not was DELIVERED alone.

Reviewing what the agent built before it ships

New journeys are drafts. Keep them that way until you've looked.

The failure mode to know about: invoking a draft returns a success code and delivers nothing. You get a runId, everything looks healthy, and no messages exist. If your agent reports a successful invoke and nothing arrives, check whether the journey was ever published.

Read the draft, look at the branches, then publish deliberately. Chapter 8 covers inspecting runs after the fact.

Rolling back

Publishing snapshots a version, and you can republish an earlier one to roll back. Runs already in flight finish on the version they started on, so a rollback never strands someone mid-sequence.

The AI node: a model as a step in the flow

An AI node runs an LLM as a step inside a running journey. Courier sends your prompt plus the run's context to the model, parses the response into structured output, and merges those fields back into the journey. Everything downstream can branch on them and render them.

This isn't "AI writes the email." It's a model producing typed values that the rest of your flow can act on.

You configure four things: the model, a web search toggle, the prompt, and an output schema. The prompt supports {{variable}} interpolation, so it can read trigger data, profile fields, and anything a fetch node pulled in.

How model output reaches the rest of your flow

The model's response is parsed as JSON and merged straight into the journey's data. Downstream nodes read those fields exactly like trigger data.

Say your schema defines subject_line, body_copy, recommended_feature, and tone. Then:

  • A branch condition reads data.tone
  • A template renders {{subject_line}} and {{body_copy}}

Courier's own worked example does exactly this: feed in a user's profile and activity, get back those four fields, branch on data.tone, and send a celebratory email on one path or an urgent push on the other.

Writing an output schema that holds up

The schema is the contract between a non-deterministic step and a deterministic branch. Loose schemas are where this breaks.

Two ways to write one. Form mode takes fields with a name, a type (string, number, or boolean), and an optional description that guides the model:

FieldTypeDescription
subject_linestringPersonalized subject under 60 characters
body_copystring1-2 sentence nudge referencing the user's activity
tonestringOne of: encouraging, celebratory, urgent

JSON mode takes a real JSON Schema, which is what you want when you need an actual enum rather than a described one. If a branch depends on the value, enforce it rather than asking politely.

Design for failure too. If an AI node fails, whether from a model error or a timeout, the journey continues and no output is merged into the context. So downstream branches have to handle the fields being absent. Give the branch a default path that assumes the AI output isn't there, and you've turned a possible dead end into a normal path.

Eight things to build with an AI node

The catalog. Each of these is a real shape, not a demo.

1. Decider. Fetch usage data, have the model classify engagement into a small set of buckets, branch on the result. The workhorse. Everything else is a variation.

2. Copywriter. Generate the subject and body per branch, so a power user and a dormant user get genuinely different messages instead of the same template with a different first name.

3. Digest summarizer. A batch or digest node collects the week's events, and the model writes the human summary. "Three people commented on your designs and Sara approved the budget" instead of a bulleted dump. This one is genuinely hard to do any other way.

4. Triage and routing. An inbound alert arrives, the model assesses severity, the branch picks the channel. Page someone for a P1, drop a P3 in the inbox. The model reads the messy description; the branch makes the decision.

5. Runtime adaptation. Adjusting a message based on context the template can't hold. Note this is not how to do translation. Use AI Translation from Chapter 5 for that.

6. Sentiment-aware follow-up. Read the last support interaction, branch on how it went, apologize on one path and upsell on the other. Nobody gets an upsell after a bad week.

7. Brand and safety check. Run generated copy past the model against your rules before the send node. On a fail, branch to a human-review exit instead of sending.

8. Chaining. Decider feeds copywriter feeds send. Works well, with one caution: if two nodes write the same key, the later one wins. Keep their schemas distinct.

Which model should an AI node use?

Start with the cheapest model that can do the job, and move up only when you can name what's wrong with the output. For constrained tasks a smaller model is often faster, more consistent, and genuinely better, because it has less room to over-elaborate on a schema you deliberately made narrow.

Reaching for the biggest model by default is the most common mistake here.

We'd start with the GPT-5.6 family. Luna, Terra, and Sol form a clean three-tier ladder that covers every job below, and moving up a tier is a single name change rather than a switch between providers.

What the node doesStart withIf that's not enough
Classify into a fixed set (risk, intent, engagement)GPT-5.6 LunaGPT-5.6 Terra
Route or triage on clear signalsGPT-5.6 LunaGPT-5.6 Terra
Extract structured fields from text you haveGPT-5.6 LunaGPT-5.6 Terra
Summarize a batch into a readable digestGPT-5.6 TerraGPT-5.6 Sol
Write customer-facing copy with tone and nuanceGPT-5.6 TerraGPT-5.6 Sol
Reason over messy or conflicting contextGPT-5.6 SolClaude Opus 5

Luna handles more than people expect: classification, routing, and extraction rarely need anything above it. Terra is the workhorse for anything that has to read well. Sol is for genuine reasoning over messy input, not for making copy sound nicer.

If you prefer Anthropic, the same ladder maps across: Claude Haiku 4.5 for the first three rows, Claude Sonnet 5 for digests and copy, Claude Opus 5 for reasoning, Claude Fable 5 when Opus isn't enough. Courier also offers GPT-5.5, GPT-5.4, GPT-5.4 Mini, GPT-5.4 Nano, and Claude Opus 4.8.

How do you know it's time to move up? Name the symptom first, because most of these are schema problems wearing a model costume:

SymptomUsually means
Fields missing, or the wrong typesSchema is too loose. Tighten it before touching the model
The same input classified differently across runsYour categories overlap. Fix the descriptions
A value outside your listed optionsMove the constraint into JSON mode as a real enum
Copy is grammatical but generic and off-brandA genuine move-up case
Reasoning is shallow on conflicting inputsA genuine move-up case

Only two of those five are actually a model problem. Change one thing at a time.

A few habits that keep cost sane without guesswork: constrain the schema before upgrading the model, gate expensive nodes behind conditions so a skipped node costs nothing, watch how much context you're sending (a large fetch response upstream becomes a cost problem downstream), and leave web search off unless the prompt genuinely needs current information. Current per-model rates are on the AI node docs page.

Testing and debugging an AI node

There's a Test panel in the node's configuration drawer. Run the prompt against the selected model with sample values and see the structured response before publishing the journey. Use it. It's much faster than publishing, invoking, and reading logs.

For a run that already happened, the step context shows which model ran, whether web search was on, and the output schema that was sent. If a node failed, the error is there too.

When not to use an AI node

Most flows don't need one.

If a branch condition or a template variable does the job, use that. It's free, instant, and deterministic. An AI node is the right call when the decision genuinely requires reading unstructured input or generating language, and the wrong call when you're using a model to compare two numbers.

Three things to weigh: every node adds latency to a running flow, every node is non-deterministic in a system that sends real messages to real people, and each one is another thing to review when output drifts.

And specifically: don't use an AI node to translate. AI Translation handles that better, as Chapter 5 covers.

Frequently asked questions

Can an AI agent safely edit a live journey?

Yes, with a review step. The pattern that works: the agent edits the draft, you read the diff, you publish. Skipping review is fine for something low-stakes like a confirmation, and risky for anything that fans out to a segment.

Why can't I create a journey with its send nodes in one call?

Every send references a template scoped to that journey, and those templates have to exist before the send can point at them. So the order is: create the journey, create the templates, wire them in, publish. Your agent handles this, but the error message makes more sense once you know why.

How do I roll a journey back?

Republish an earlier version. The journey id stays the same, and runs already in flight finish on the version they started on, so nobody gets stranded mid-sequence.

What happens to a user mid-journey if I publish a change?

They finish on the version they started on. New runs pick up the new version. The same is true of journey templates: editing one after publishing creates a draft, and you re-publish the journey to make it live.

What can the AI node do that a branch condition can't?

Read unstructured input and produce a decision from it. A branch compares values you already have. An AI node turns a support transcript, a usage pattern, or a free-text description into a typed field the branch can then compare. If you already have the field, use the branch.

Why did my AI node produce nothing?

Most often the node failed and the journey carried on, because a failed node merges no output into the context. Check the step context for the error, then make sure your downstream branch has a default path that handles the fields being missing.