Part 3 of 7 · Job status notifier series ~7 min read

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.changed event 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.

Composing one update: gather the facts, one Bedrock call, check the draft, fall back if needed Inside a dotted AWS account container. On the left, a box “stage.changed” arriving from the bus into an SQS queue box “jsnr-notify (+ DLQ)”, which triggers a box “jsnr-composer”. The composer first gathers facts, shown as three small input boxes feeding into it: “new stage” from the event, “name, item, channel, ETA” from the jsnr-jobs table (a cylinder), and “photo” from the S3 photos bucket. From the composer an arrow labelled “facts only” goes to a box “Bedrock Haiku 4.5” that returns a draft. The draft enters a check box “names the real stage? no invented ETA?” with two exits: pass goes to “send” leaving the container toward the sender; fail or model error routes to a box “plain template from the facts” which also goes to send. A note reads: facts come from the record, words come from the model — if the draft is off, a true plain line goes instead. AWS account stage.changed from jsnr-bus jsnr-notify SQS + DLQ jsnr-composer gather facts first new stage (event) jsnr-jobs: name, ETA S3 photo Bedrock Haiku 4.5 facts only check draft real stage? no fake ETA? draft plain template true, from the facts fail / error send to sender pass Facts come from the record, words come from the model — if the draft is off, a true plain line goes instead.
Fig 3. The composer gathers the facts before any model runs, makes one Bedrock call to phrase them in the shop’s voice, then checks the draft names the real stage and invents no ETA. A failed check or a model error falls back to a plain, true line.

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.
All posts