Chapter 4
Most bad output from an agent isn't a model problem. It's a missing-information problem. You asked for a welcome email and didn't say which channels are live, who the recipient is, or what data you have, so the agent picked something plausible.

Last updated: September 2026
Most bad output from an agent isn't a model problem. It's a missing-information problem. You asked for a welcome email and didn't say which channels are live, who the recipient is, or what data you have, so the agent picked something plausible.
Close that gap and the output gets better immediately. Then check it anyway.
Five things. Recipient, channels, trigger, data, constraints.
You can derive this from the skill itself, which tells the agent to stop and ask when a request is underspecified: "Channel? Transactional or lifecycle? New code or existing? Which language?" Those are the questions a complete prompt has already answered.
user_id, not an email address. It's how preferences, routing, and cross-channel delivery find each other.Here's the difference, concretely.
The vague version:
Add a welcome email when someone signs up.
What you get back is reasonable and mostly unusable. An email-only template, hardcoded copy, a send to an inline email address, no user_id, no preference topic, published straight to live because nothing said not to. It works in a demo and breaks the first time someone updates their email address.
The complete version:
Add a welcome notification for new signups. Send to
user_id. Email is live via SendGrid and we have the in-app inbox; no SMS yet. Trigger it fromhandleSignupinsrc/auth/signup.ts. Available data isfirst_name,plan_name, andworkspace_url. Keep the subject under 50 characters. Create it as a draft, don't publish, and show me the rendered output before we ship it.
Same model, same skill, completely different result. You get a template that renders on both live channels, a send keyed to user_id, the trigger wired into the right function, and a draft waiting for review.
The second prompt is four sentences longer.
Guessing is what happens when an agent needs a fact it doesn't have. Give it the facts, in order of impact:
1. Install the skill. It carries the verified call shapes and the rules that are easy to get wrong. One command, covered in Chapter 2.
2. Give it the docs. Courier publishes machine-readable documentation specifically so agents can read it:
https://www.courier.com/llms.txt is the briefing: what Courier is, when to use it, how agents should approach ithttps://www.courier.com/docs/llms.txt is the documentation indexhttps://www.courier.com/docs/llms-full.txt is every page, concatenatedIf you installed the skill as a Claude Code plugin, the docs MCP server comes with it and your agent can look things up without being told to.
3. Write down what's true about your setup. The AGENTS.md from Chapter 2. Which environment, which channels are live, what the review rule is.
An agent with all three stops asking you the same question every session and stops inventing answers when you're not looking.
An agent will tell you it sent the message. It's reporting that the API call succeeded, which is not the same claim. A 200 with a requestId means Courier accepted the job. Delivery is a separate question with a separate answer.
So ask for the answer:
Show me the status of every message sent to user_test today, and the current step of the onboarding journey run.
That's from Courier's own agent quickstart, and the reason it's the right prompt is in the docs' framing of it: the answer comes from Courier, not from the agent's memory.
Three checks worth building into your habits:
Did it render correctly? GET /messages/{message_id}/output returns exactly what Courier handed the provider: resolved variables, brand styling, the real subject line, per channel. Not a description of the output. The output.
courier messages content --message-id <MESSAGE_ID>
Did it actually go? The history endpoint is the honest one. A trailing SENT event is the real confirmation.
courier messages history --message-id <MESSAGE_ID>
If it failed, why? There's a ladder, and it goes in this order:
courier messages list --recipient user_test # find itcourier messages history --message-id <ID> # what happenedcourier messages content --message-id <ID> # what it tried to send
Build these into the prompt itself. "Send it, then show me the delivery history and the rendered output" is one sentence longer and removes the entire category of an agent confidently reporting success on a message that never arrived.
Some of these come from Courier's own "what not to do" list, and some are ours. All of them are mistakes an agent makes more often than a person does.
PUT /profiles/{user_id} for updates. PUT replaces the whole profile, and any field you leave out is deleted. POST merges. An agent updating a phone number with PUT will quietly drop the email address.'Idempotency-Key' on the send.method: "all" for OTPs, password resets, or billing. Use "single" with fallback channels, or the user gets the same code three ways.user_id with no stored profile if the channel needs contact info. Courier won't know where to deliver it.test@example.com. Reserved domains are blocked and come back UNDELIVERABLE, which looks like a bug and isn't.Two moments, and they're both about blast radius.
Publishing. Templates and journeys are created as drafts by default. Keep it that way. An agent that can publish is an agent that can push untested copy to everyone, and reviewing a draft costs thirty seconds.
Anything that fans out. A single test send is low risk. A journey that triggers for every user in a segment is not. The review step belongs between "the agent built it" and "it runs for real."
Scope your keys so the blast radius is small by default: one key per agent, per environment, so revoking one doesn't take down everything else.
For this work, yes, in two places.
Structured output is where it shows up first. An agent that reliably produces valid JSON against a schema saves you a debugging loop. Most current models are fine at this; the older and smaller ones drift.
The second place is inside your journeys, where an AI node runs a model as a step in a live flow. That's a different decision with different tradeoffs, and Chapter 6 covers which model to pick for which job.
Recipient type, which channels are live, the trigger, the data fields available at send time, and any constraints like subject length or whether a human publishes. Four sentences covering those five things is the difference between a demo and something you'd merge.
Ask it to show you the delivery history, not to tell you it worked. courier messages history --message-id <ID> returns the real event sequence, and a trailing SENT is the confirmation. A 200 on the send call only means Courier accepted the job.
Once you trust the flow, yes, and plenty of production systems work this way. Get there in stages: Test environment first, then Production with a human publishing anything that fans out. Keep a separate API key per agent so you can revoke one without revoking everything.
Install the skill, point it at llms.txt, and put your own setup in an AGENTS.md. Agents invent things when they need a fact they don't have. Most of the time the fix is supplying the fact rather than writing a better prompt.
Previous chapter
Skill, MCP, CLI, or SDK: When to Use Which
You now have four ways to get Courier work done. They're not alternatives to each other, and picking by habit is how people end up running a model inside a deploy pipeline.
Next chapter
Designing Templates with AI
The reason cross-channel templates matter isn't elegance. It's that the alternative is writing the same message four times and keeping four copies in sync forever. One document, many renderings, and an agent that can produce it from a sentence.
© 2026 Courier. All rights reserved.