Community Article
Community articles are authored by SitePoint Premium contributors. Content is screened before publication, and SitePoint reserves the right to moderate or remove articles that violate our guidelines. Views expressed are those of the authors and do not necessarily reflect those of SitePoint.
Building a Reliable After-Hours Call-Routing Workflow with TypeScript
SASaifullah AdenwallaPublished inNode.js·JavaScript·AI·APIs·Browser Tools·
October 14, 2026
·Updated:October 14, 2026
The AI briefing for Developers
Stay up to date with AI tools, model releases, and developer workflows that matter.
Weekly. Free. One click to leave.
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
A call arrives at 2 a.m. Your webhook returns a successful response, a notification provider accepts a message, and the dashboard turns green. Has anyone actually taken responsibility for the request?
Those are three different events. Treating them as one is an easy way to build a workflow that looks healthy while work remains unattended.
After-hours answering systems provide a useful example of a broader web-development problem: coordinating external events, background jobs, and human acknowledgement. The same patterns apply to customer support, maintenance dispatch, and incident response.
This tutorial develops a small TypeScript state model and explains how to connect it to durable storage, secure webhooks, and an accessible operator interface. The code models operational handoffs using synthetic identifiers. It does not assess symptoms, determine medical urgency, or implement a clinical triage system.
1. Define what each status actually guarantees
Start with observable events rather than a vague “handled” flag. A provider accepting a notification proves only that it accepted the request. A delivery receipt still does not establish that an authorized person accepted responsibility.
queued: the application has durably recorded the request.
notified: the notification provider accepted an outbound attempt.
acknowledged: an authenticated, authorized operator accepted ownership.
escalated: the acknowledgement deadline passed and fallback handling is required.
closed: the responsible operator completed the operational workflow.
Keep clinical decisions outside this model. In a healthcare integration, the responsible clinical organization must define approved pathways and the people authorized to use them. Software should preserve those boundaries rather than infer clinical capability from a service name.
Define ownership too: who maintains the routing policy, who receives fallback alerts, and who resolves an unassigned request? A queue with no accountable owner is only a storage mechanism.
When a project involves an external team such asHelpSquad Health, define the handoff contract before building the integration: which events the team receives, how an operator confirms ownership, and what happens when nobody acknowledges a request. Confirm the available integration methods directly with the service; the code below is a provider-independent example.
2. Implement a small, explicit state machine
Separate the decision about a state transition from network calls and database writes. A pure function is easier to test because the same inputs produce the same output.
Save the following as routing.ts. The deadline is supplied by application policy; it is not a recommended healthcare response time.
Several choices are deliberate. A late provider callback cannot move an acknowledged case back to notified. An acknowledgement can arrive before the provider callback and still be accepted. Closed cases remain closed if old events are replayed.
This function assumes events have already passed authentication, authorization, and runtime validation. A TypeScript type alone cannot make an incoming HTTP request trustworthy. The API must also check that the actor belongs to the correct organization and is allowed to claim that particular case.
For larger workflows, statechart tooling can help express parallel or nested states. SitePoint’s article ondeterministic state machines in TypeScript provides related architectural reading. The example here intentionally keeps the operational model small.
3. Validate and authenticate incoming events
Place a provider adapter between the webhook and the domain function. The adapter translates provider-specific names into your internal event vocabulary. That keeps transport details out of the routing rules.
For projects involvinghealthcare outsourcing, make the boundary between the application and the external team explicit. Document which system owns the case record, which staff can update its status, and how access is removed when an assignment ends. These decisions shape the authorization rules and audit trail before any transport adapter is written.
Use a runtime schema library such asZod to check the fields the adapter needs: a bounded event identifier, a case identifier, an allowed event type, and any required timestamp. Reject malformed input before it reaches the state machine. Limit request sizes as well.
Schema validation answers “does this input have the expected shape?” Signature verification answers a different question: “did the expected provider sign this request?” Neither replaces application authorization.
Follow the provider’s exact signature format, ideally through its supported SDK. Some schemes depend on raw request bytes; others include the public URL and parsed parameters. Reverse proxies can change the URL your application sees, so configure this deliberately. TheTwilio webhook security documentation illustrates why provider-specific validation matters.
SitePoint’scryptography guide for Node.js developers is useful background for signed requests. Do not substitute a generic signature snippet for the actual provider contract.
Keep acknowledgement on a separate authenticated application endpoint. A caller-controlled payload or delivery receipt must never manufacture an operator’s acceptance of responsibility.
4. Make repeated delivery safe with durable transactions
Assume a webhook may arrive more than once. An in-memory set of event IDs will not survive a restart and will not coordinate multiple server instances.
A database uniqueness constraint can prevent duplicate processing across instances. Scope identifiers to the provider account that issued them, rather than assuming every provider uses globally unique IDs.
CREATE TABLE ON CONFLICT DO NOTHING
PostgreSQL’sINSERT documentation describes conflict handling and RETURNING. Here, no returned row means the event was already recorded. This is a SQL fragment; $1 and $2 must be supplied as parameters by your database client.
The important part is the transaction around that fragment. Insert the event, lock the corresponding case row, apply the transition, record the audit entry, and insert any outbound work into an outbox table. Commit them together. If processing fails, roll back the event marker too, so a retry can do the work.
An outbox is a table of pending side effects written in the same transaction as the state change. A worker later reads it and contacts the notification provider. This avoids the gap where the application commits a case update but crashes before creating the notification job.
Outbox delivery can still repeat. If the provider accepts a request and the worker crashes before recording success, the worker may send it again. Use a stable idempotency key when the provider supports one; otherwise document the possibility of duplicate notifications and implement reconciliation. Do not promise exactly-once external delivery.
For another application of these ideas, see SitePoint’sevent-driven pipeline with Node.js and PostgreSQL.
5. Persist deadlines and handle races explicitly
A process-local setTimeout is useful for a demonstration, but it disappears when the process exits. Persist deadlineAt and have a durable worker find overdue, unacknowledged cases.
When a deadline job runs, it must read the current case state. A job created earlier may now be obsolete because somebody acknowledged the request. Lock the case row, recheck its state and deadline, and persist the escalation and fallback outbox item in one transaction. Multiple workers must use the same concurrency discipline.
Acknowledgement and escalation can arrive together. With row locking, one transaction completes first and the next sees its result. If acknowledgement wins, the deadline check becomes a no-op. If escalation wins, a later acknowledgement can still claim the case.
Even a second state check immediately before sending cannot eliminate every race with an external provider. Display the current owner prominently so a recipient opening a late notification can see that the request has already been claimed.
Use a consistent server-side time only for display. If routing depends on an on-call calendar, test overnight shifts, holidays, and daylight-saving transitions separately from the state machine
6. Design the dashboard around responsibility
Show the current owner, acknowledgement status, last successful update, and any overdue deadline. Avoid a single green “sent” label that conceals the difference between transport success and human acceptance.
Use visible text in addition to color. A suitable acknowledgement control might read “Accept responsibility,” followed by a confirmed owner name after the server accepts the update. Keep it pending while the request is in flight; restore it with an actionable error if the write fails.
An accessible status region can announce the result without moving keyboard focus:
<p role=”status” aria-live=”polite”>
Acknowledged — assigned to the on-call operator.
Do not place a constantly changing countdown inside that live region. Announce meaningful state changes, while rendering routine time updates elsewhere. If the dashboard loses its connection, label the displayed information as stale and provide a retry action.
Keep operational identifiers separate from sensitive case content. Logs and notification previews usually need an opaque case ID, event type, outcome, and timestamp. Avoid copying transcripts or personal details into analytics, URLs, or generic error reports. Access checks still apply when someone follows a case link.
7. Test failure sequences before connecting live services
The most useful tests describe what must remain true when events arrive late or in an unexpected order. With Vitest, create routing.test.ts alongside the state model:
import { expect, test } from “vitest”;
import { transition, type Case } from “./routing”;
In a TypeScript project with Vitest installed, run npx vitest run. These tests exercise the pure model; they do not validate the database, webhook adapter, or notification provider. Passing now explicitly avoids sleeping in tests. For code that uses timers directly, Vitest documentsfake timers.
Add integration tests that deliver the same event twice, restart a worker after committing an outbox item, and race two operators claiming the same case. Verify persisted state and outbound effects, not just HTTP response codes.
UsePlaywright’s web-first assertions for the dashboard: accept a case, wait for the confirmed owner, simulate a failed update, and verify that stale information is clearly marked. Its retrying assertions are preferable to arbitrary delays.
During development, use synthetic cases and a sandbox provider account. The state machine, SQL fragment, and tests are building blocks rather than a complete deployable service. Production work still includes authorization, migrations, retry policy, audit retention, monitoring, and operational review.
Make the handoff observable
The central engineering question is simple: can you prove who owns an unfinished request right now? Answer it with explicit states, durable events, transactional updates, and an interface that distinguishes delivery from acknowledgement.
Start by implementing the pure model and its tests. Then add persistence and deliberately interrupt the workflow between each step. A system that survives a duplicate callback, a worker restart, and a late acknowledgement teaches more about reliability than a successful happy-path demo.


