Part 2 of 7 · Job status notifier series ~8 min read

How a job stage gets updated

Before a customer can be told their job moved, the system has to be sure it actually did. This post is about that step alone: how a quiet poll of the board turns “the stage cell changed” into one trustworthy stage-change event — and how it refuses to fire on a typo, a sideways edit, or the same move seen twice.

Key takeaways

  • A small watcher polls the board every few minutes and compares each job’s stage against the last one it recorded.
  • A change only counts if it’s a known stage and a step forward — not a typo, a sideways edit, or a move backwards.
  • The same poll seen twice never fires twice; the last-recorded stage is the dedupe key.
  • A genuine move is appended to a stage-history table, then emitted as one stage.changed event on a custom bus.
  • Detecting the move and sending the message are split, so a slow message can never hold up watching the board.

Watching a board that wasn’t built for this

The job board is whatever the shop already uses — usually a spreadsheet or the board view of a job tool. It was built for people to look at, not for a machine to listen to, and that’s the whole challenge of this step. There’s no neat “a job moved” signal to subscribe to. All the system has is a grid of jobs with a stage written in a cell, and that cell quietly changing whenever someone at the counter drags a card or edits a row.

So the watcher does the only reliable thing: it looks at the board on a schedule and remembers what it saw last time. A small Lambda, jsnr-stage, runs every few minutes on an EventBridge Scheduler rule, reads the current stage of every open job, and compares each one against the stage it recorded in the jsnr-jobs table on its previous pass. Most passes find nothing changed and do nothing. When a job’s stage no longer matches what was recorded, that job is a candidate for a move — but a candidate is not yet a message.

When a change actually counts

A cell changing is not the same as a job moving forward. People fix typos, correct a mistaken drag, or relabel a stage. If every edit fired a customer text, the system would be worse than useless — it would be a liar that texts “your job is ready!” because someone fat-fingered the wrong row. So a candidate change has to clear three checks before it counts:

  • Is it a known stage? The new value has to be one of the valid stages listed in the shop settings — received, diagnosing, in progress, awaiting parts, ready, collected. A stray value (“in pgoress”, a blank, a note someone typed in the wrong cell) is not a stage. It’s logged and ignored, and nothing is sent.
  • Is it a step forward? Stages have an order. A move from received to in progress is forward and notifiable; a move from ready back to in progress is a correction, not news for the customer. Backward and sideways moves are recorded in the history for the shop’s own record but don’t send a message — nobody wants a text saying their finished job is somehow unfinished again.
  • Have we already seen it? Because the watcher polls, the same new stage will be sitting there on the next pass too. The last-recorded stage in jsnr-jobs is the dedupe key: once a move to in progress has been recorded and emitted, the stage is in progress as far as the watcher is concerned, so the next poll sees no change and stays quiet. One move produces exactly one event.

Only a candidate that is a known stage, a step forward, and genuinely new survives all three. That is what the rest of the system treats as “the job moved.”

Record first, then announce

When a change counts, the watcher does two things in order. First it writes the move to jsnr-stage-history — an append-only row keyed by job and timestamp, capturing the from-stage, the to-stage, and when it happened — and updates the last-recorded stage on the job. That write is the system’s memory; it’s what makes the dedupe work and gives the shop an honest timeline of every job later. Only then does it put a single stage.changed event onto a custom EventBridge bus, jsnr-bus, carrying the job id, the new stage, and the timestamp.

Recording before announcing matters. If the history write succeeds but the event somehow fails, the worst case is a missed message, not a wrong one — and the next poll won’t re-fire, because the stage is already recorded as moved. The system would rather stay quiet than tell a customer something that isn’t backed by a recorded fact.

From a board poll to a single stage-change event, with the three validity checks On the left, a box “The job board” representing the shop’s spreadsheet of jobs and stages, sitting outside a dotted AWS account container. Inside the container, an EventBridge Scheduler box (“every few minutes”) triggers a box “jsnr-stage poll”, which reads the board and compares each job’s current stage against the last recorded stage held in the jsnr-jobs table (drawn as a cylinder). The comparison flows into a decision diamond region labelled “changed?”; if no change, an arrow loops back to wait for the next poll. If changed, the candidate passes through three stacked checks: “known stage?”, “step forward?”, and “not already seen?”. A failing check routes to a box “log and ignore — no message”. A candidate that passes all three flows to a box “append to jsnr-stage-history” (drawn near a second cylinder), which then emits to a box “stage.changed on jsnr-bus”, with an arrow leaving the container toward the rest of the system. A note reads: a cell changing is not a job moving — only a known, forward, not-yet-seen change becomes an event. The job board stages in cells AWS account Scheduler every few minutes jsnr-stage poll compare stages read jsnr-jobs last stage seen vs. known stage? step forward? not seen yet? changed log & ignore no message any fail history jsnr-stage-history pass stage.changed on jsnr-bus out A cell changing is not a job moving — only a known, forward, not-yet-seen change becomes one event.
Fig 2. The watcher polls the board, compares each stage against the last one it recorded, and only a known, forward, not-yet-seen change survives. The move is written to history, then emitted as a single stage.changed event.

Why a poll, and why an event

Two design choices in this step are worth defending. The first is polling rather than waiting for the board to push. Spreadsheets and most lightweight job tools simply don’t offer a reliable “tell me when a cell changes” hook, and the ones that do are fiddly and break quietly. A poll every few minutes is boring, cheap, and robust: if a poll is missed, the next one catches up, because the truth is always sitting in the board. A few minutes’ delay between a counter staff member moving a card and the customer getting a text is completely fine for this job — nobody is timing it to the second.

The second is splitting detection from sending by putting an event on a bus rather than calling the composer directly. The watcher’s job is to watch the board and get out of the way; composing and sending a message — which involves a model call and a network send — is slower and can fail. By emitting a stage.changed event and letting a rule route it onward (through a queue, in Part 3), a slow or failed message never blocks the next poll, and a message that fails can be retried without re-reading the board. The watcher stays a fast, simple sensor; everything that might be slow lives downstream.

Design rules that shaped stage detection

  • The board is read, never trusted blindly. A changed cell is a candidate, not a confirmed move.
  • Three checks, every time. Known stage, step forward, not already seen — or it doesn’t fire.
  • The last-recorded stage is the dedupe key. One move produces exactly one event, however often we poll.
  • Record before you announce. History is written first; the worst failure is a missed message, never a false one.
  • Detection and sending are separate. The watcher emits an event; nothing slow gets to hold up the board.
All posts