How a customer update gets composed
By the time this step runs, the system knows a job has genuinely moved to a new stage. All that is left is to say it like the shop would. This post is about the only place a model is allowed near the system: the single Bedrock call that turns real stage facts into one warm, accurate message — and the fences that keep it honest.
Key takeaways
- The
stage.changedevent is routed through an SQS queue to the composer, so a slow model call never blocks the watcher. - The composer gathers the facts first — the new stage, the next-stage ETA, the photo — before any model runs.
- One Bedrock Haiku 4.5 call turns those facts into a single message in the shop’s voice. The model only phrases; it never decides.
- The draft is checked against the facts: it must name the real stage, and it can’t claim an ETA the facts didn’t carry.
- If anything is missing or the draft fails the check, the system sends a plain templated line rather than something invented.
From an event to a set of facts
A rule on the bus matches every stage.changed event and drops it onto an SQS queue, jsnr-notify, which triggers the composer, jsnr-composer. The queue is doing quiet but important work: it absorbs a flurry of moves at opening time without dropping any, and if a message fails it can be retried — after a few attempts a poison message lands in a dead-letter queue rather than spinning forever. The watcher from Part 2 has already moved on; composing happens on its own time.
The first thing the composer does is not call a model. It gathers facts. From the event it has the job id and the new stage. From jsnr-jobs it pulls the customer’s name, what the job is, and their channel. From the shop settings it works out the honest ETA for the next stage — the mechanics of that are Part 4. And it checks the photos bucket for a picture attached to this job at this stage. That bundle — stage, item, name, ETA, photo — is the complete, factual basis for the message. Everything in the text that follows has to come from this bundle and nowhere else.
The one place a model runs
Composing the wording is the only place in the whole system a model is allowed near. It’s a good fit for one: the difference between “Status: IN_PROGRESS” and “Good news — we’ve started on your repair and expect it ready Thursday” is exactly the warmth a small shop wants and a template can’t fake. So jsnr-composer makes a single Bedrock Haiku 4.5 call, handing the model the gathered facts and the shop’s voice from settings, with strict instructions: use only these facts, name this exact stage, mention the ETA only if one is given, keep it to a sentence or two, and write it the way this shop talks.
The crucial line in that prompt is the fence: the model is told what stage the job is in and is forbidden from changing it. It does not get to decide that “in progress” really means “nearly done”, or to promise a day the settings didn’t supply. It is a writer handed the facts, not a clerk deciding them. The stage came from the board; the ETA came from the settings; the model just makes them sound human.
Checking the draft before it leaves
A model handed strict facts is still a model, so the draft doesn’t go out unread. The composer runs the returned text through a couple of plain checks before passing it to the sender. It confirms the message refers to the stage the job actually moved to — a draft that somehow talks about being “ready” when the stage is “in progress” is rejected. And it confirms the message doesn’t assert a collection time or date unless an ETA was in the facts; if no ETA was supplied, the message must not invent one.
If the draft passes, it’s the message. If it fails either check, or if the model call itself errors or times out, the composer falls back to a plain templated line built straight from the facts — “Your iPhone screen repair is now in progress. We’ll let you know when it’s ready.” It’s less warm, but it is always true. The system would rather send something flat and correct than risk something charming and wrong. A customer is never the test subject for a model’s bad day.
What a composed message looks like
The same facts, the same shop, two different moves. When J-2042 reaches in progress with a Thursday ETA and a photo, the composer sends: “Hi Maya — we’ve started on your iPhone screen repair and expect it ready to collect by Thursday afternoon. Here’s a quick look at the work: [photo]. Any questions, just reply. — The Repair Bench.” When it reaches ready, with no further stage to estimate: “Great news, Maya — your iPhone is repaired and ready to collect any time before 6pm today. See you soon!” Both are warm, both name the real stage, and the second one carries no invented “next” date because there isn’t one. That restraint — saying only what’s true — is the whole point of feeding the model facts and checking what it gives back.
Because the wording, the voice, and the rules all live in the prompt and the settings rather than in code, the shop can change how it sounds without a deploy. Switch the voice from breezy to formal, add a sign-off, ask for emoji or ban them — it’s an edit to the settings doc, and the next message follows. The facts are locked down; the personality is free.
Design rules that shaped the composer
- Gather facts before calling the model. The message is built on the record, not on the model’s imagination.
- The model phrases, it never decides. Stage and ETA are handed in; the model only makes them sound human.
- Name the real stage, invent no ETA. The draft is checked against the facts before it can leave.
- A true plain line beats a charming wrong one. Any failure falls back to a template built from the facts.
- Voice lives in settings. How the shop sounds changes without a deploy; what’s true never bends.