Backend Error Tracking Evidence for Silent Cron Jobs and AI Tutoring Workers An engineer running a small edtech backend outlined an approach for tracking silent failures in scheduled lesson-planning jobs and AI tutoring agent loops by combining structured exception events with explicit run-completion signals tied to a shared runId. The design records queued, started and completed events per run, attributes model usage and latency at each agent step, and treats a missed completion deadline as the primary alert, with captured exceptions serving as supporting evidence about cause. The engineer argued this gives a solo operator enough context to tell whether a slow or expensive run is also broken, without maintaining an elaborate telemetry pipeline. A small edtech backend needs one failure record that connects each scheduled lesson-planning run, background worker step, and web request to the AI agent loop's latency and cost. The least complex workable choice is exception capture plus explicit run completion signals, joined by a stable run ID. Exception capture alone cannot prove that expected work happened. A heartbeat alone cannot explain which agent step failed. TL;DR: choose the approach that preserves both negative evidence an exception occurred and positive evidence the expected run started and finished . Attribute model usage at each agent step, then roll it up by run, course, and tenant. That gives a solo operator enough context to search, resolve, and decide whether a slow or expensive run is also broken. | Approach | Thrown errors | Missing or stuck work | Agent-loop cost attribution | Operational load | |---|---|---|---|---| | Exception capture only | Strong | Weak | Weak unless enriched | Low | | Completion checks only | Weak | Strong for scheduled work | Weak | Low | | Structured events plus completion checks | Strong | Strong | Strong with step-level usage | Moderate | | Metrics and logs assembled during an incident | Variable | Variable | Possible, but slow to reconstruct | High | The recommendation is structured exception events plus completion checks. Keep the event schema small. A one-person SaaS should spend its engineering hours on the tutoring workflow, not on maintaining an elaborate telemetry pipeline. A web API can return a successful response while enqueueing work that never completes. A cron process can exit cleanly after selecting zero rows because its filter is wrong. A worker can catch an exception, acknowledge the message, and quietly discard the only useful stack trace. These are different failures, even if a user sees the same result: no generated lesson plan. Start with the expected state transition. For one planning run, define queued , started , and completed events. Record an exception event if execution fails. The absence of completed after the workload's deadline is itself a signal; no thrown exception is required. This is why a generic process-alive check is insufficient. It says the worker can answer, not that a particular lesson-planning run crossed its boundary. Google SRE's monitoring guidance separates symptoms from causes and frames latency, traffic, errors, and saturation as core signals. For this workload, a run deadline is the symptom-facing error signal, while the captured exception is evidence about cause. Keep both. Consider one concrete run. The API accepts a teacher's request, assigns runId plan 42 , and queues the planner. The first worker spends 800 ms classifying the assignment, then a second step retrieves curriculum context. A retry starts after the worker lease expires. Nothing throws at the web boundary, and a process health check stays green throughout. Without run events, the operator sees a successful request and two unrelated model-usage entries. With the shared ID, the timeline shows two attempts, no terminal completion, and usage attached to the unfinished run. The alert should come from the missed completion deadline. The exception, if one exists, should remain supporting evidence. This distinction matters for an agent loop because a silent retry can add latency and cost before anyone sees a stack trace. One ID. Two kinds of proof. An AI agent loop may call a model several times: classify the assignment, retrieve curriculum context, draft a response, and verify it. A total attached only to the HTTP request hides retries and background continuation. A total attached only to a worker hides the originating tenant and course. Use one runId across the request, queue message, worker, and scheduled reconciliation job. Give each model call a step and record its duration plus the usage values returned by the model provider. Do not estimate usage from string length when authoritative usage is available. Also keep the model identifier actually returned or selected for that call; attribution without that dimension makes later comparisons ambiguous. The useful unit is an agent step. The useful rollup is a run. This split lets an operator answer two separate questions: "Which step made this run slow?" and "Which incomplete runs still incurred model usage?" A revenue-per-hour lens changes the implementation threshold. I would accept a plain append-only event sink and a daily aggregate before building a bespoke analytics layer. Ship weekly. Outsource undifferentiated storage and alert delivery when the contract allows data export and stable identifiers. The differentiating work is the tutoring loop and the rules that decide when a generated answer is safe to show. The instrumentation boundary should be boring. It should also survive a queue hop. The following example emits structured events through a generic sink; the same envelope can go to a log appender, an event stream, or a database table. type RunEvent = { runId: string; tenantId: string; courseId: string; source: "api" | "cron" | "worker"; stage: "queued" | "started" | "model call" | "completed" | "failed"; step?: string; durationMs?: number; inputTokens?: number; outputTokens?: number; model?: string; errorType?: string; occurredAt: string; }; interface EventSink { write event: RunEvent : Promise