# How DeskcommCRM Schedules, Retries, and Observes Follow-ups Using Workers and Event-Log Consumers

> Learn how DeskcommCRM schedules, retries, and observes follow-ups using cron workers and event log consumers. Understand the process from cron jobs to agent worker queues.

- Repository: [Rafael Melgaço/DeskcommCRM](https://github.com/melgarafael/DeskcommCRM)
- Tags: internals
- Published: 2026-09-13

---

**DeskcommCRM orchestrates follow-up delivery through a dedicated `followup_turn` job type that moves from the `cron_jobs` table through a lightweight cron worker into the agent worker queue, with deterministic retry logic handled by the anti-ban guard's `rescheduleReentry` mechanism.**

The follow-up system in DeskcommCRM relies on a decoupled pipeline that separates time-based scheduling from heavy agent execution. By leveraging the `cron_jobs` table and specialized workers, the platform ensures reliable delivery of promised responses while handling temporary blocks through idempotent retry mechanisms.

## Scheduling Follow-ups via the Cron Jobs Table

When a tool such as `crm_schedule_followup` creates a follow-up promise, the engine writes a row into the `cron_jobs` table with `kind = 'at'` and `job_kind = 'followup_turn'`. This row stores the promise data—including `reason`, `promise`, `promised_at`, and a `service_boundary` pointing to the original conversation—effectively creating a future-dated trigger for the follow-up logic.

### The scheduleCronJob Helper

The `scheduleCronJob` helper abstracts the database insert and guarantees idempotency when creating these entries. This utility is called directly from the follow-up engine and manages the payload construction that will later be consumed by the workers.

```typescript
await scheduleCronJob(pool, tenantId, {
  leadId,
  spec: { kind: 'at', at: promisedAt },
  jobKind: 'followup_turn',
  payload: {
    reason,
    promise,
    promised_at: promisedAt.toISOString(),
    followup_enrollment_id: enrollmentId,
    // …other fields
  },
});

```

## The Cron Worker Bridge

A lightweight HTTP handler at [`app/api/v1/cron/followup-flow-worker/route.ts`](https://github.com/melgarafael/DeskcommCRM/blob/main/app/api/v1/cron/followup-flow-worker/route.ts) serves as the bridge between the scheduling layer and the execution queue. When the scheduled `at` time arrives, this cron worker enqueues the `followup_turn` job into the main `job_queue`, decoupling the time-based trigger from the heavy agent runtime.

This design prevents the cron system from blocking on long-running agent operations. The cron worker performs only the enqueue operation, ensuring that the actual follow-up logic executes within the robust agent-worker environment.

## Event-Log Consumer and Agent Worker Processing

The agent worker daemon registers a specific handler for `followup_turn` jobs in [`workers/agent-worker/main.ts`](https://github.com/melgarafael/DeskcommCRM/blob/main/workers/agent-worker/main.ts) using `handlers.set("followup_turn", …)`. This registration enables the event-log consumer to route follow-up jobs to the appropriate processing logic implemented in [`lib/agent-engine/agent/followup-turn.ts`](https://github.com/melgarafael/DeskcommCRM/blob/main/lib/agent-engine/agent/followup-turn.ts).

### Handler Registration in the Agent Worker

The handler registration occurs during worker initialization, binding the `createFollowupTurnHandler` function to the `followup_turn` job type:

```typescript
handlers.set('followup_turn', createFollowupTurnHandler(turnDeps));

```

The `createFollowupTurnHandler` function fetches the target conversation, validates that the channel is not archived, and delegates execution to the shared `runAgentTurn` logic.

### Flow-Driven vs Classic Turn Execution

Within the handler, the system checks for the presence of `followup_enrollment_id` in the payload. When this identifier exists, the turn executes via `runFlowDrivenTurn`, enabling structured conversation flows. Otherwise, the system follows the classic "template or agent" execution path, allowing for dynamic response generation based on the original conversation context.

## Retry Logic and Anti-Ban Protection

During the outbound send phase within `sendFixedOutbound`, the **anti-ban guard** monitors the message dispatch. If the guard vetoes the message with `chain.status === 'vetoed'`, specific retry logic activates based on the veto reason code.

### The Veto Mechanism in sendFixedOutbound

When the veto reason is `outside_window`, the system determines that the current time falls outside acceptable messaging windows (such as compliance hours or rate limits). Rather than failing the job permanently, the system captures the veto and prepares for deferred execution.

### Idempotent Rescheduling with rescheduleReentry

The `rescheduleReentry` function (implemented around lines 1080-1086 in the source) creates a one-shot `cron_jobs` entry of kind `at` with the same payload plus a `reschedule_of` marker. This ensures idempotent retry even if the worker crashes mid-operation:

```typescript
if (chain.status === 'vetoed' && chain.code === 'outside_window') {
  await rescheduleReentry(pool, {
    tenantId,
    leadId,
    jobId: job.id,
    at: chain.nextAllowedAt,
    payload: job.payload,
  });
}

```

The new `cron_jobs` row preserves the original payload while updating the execution time to `chain.nextAllowedAt`, allowing the cron worker to pick up the job again when appropriate.

## Observation and Completion Tracking

After a successful send—or after classification and timing plan adjustments—the handler invokes the bridge callback `completeFollowupTurn` located in [`lib/followup/turn-bridge.ts`](https://github.com/melgarafael/DeskcommCRM/blob/main/lib/followup/turn-bridge.ts). This callback records the final result in the `followup_enrollments` table and removes the job from the queue.

The **event-log consumer** (the same agent-worker process) can observe these state transitions through the database updates, enabling monitoring and debugging via queries to the `cron_jobs` table (facilitated by utilities in [`lib/followup/retorno-crm.ts`](https://github.com/melgarafael/DeskcommCRM/blob/main/lib/followup/retorno-crm.ts)). This creates a complete audit trail from initial scheduling through final delivery or failure.

## Summary

- **Scheduling**: Follow-ups are inserted into `cron_jobs` as `followup_turn` entries with `kind = 'at'` using the `scheduleCronJob` helper, ensuring idempotent creation.
- **Cron Worker**: The [`app/api/v1/cron/followup-flow-worker/route.ts`](https://github.com/melgarafael/DeskcommCRM/blob/main/app/api/v1/cron/followup-flow-worker/route.ts) endpoint moves scheduled entries into the active `job_queue` without blocking on execution.
- **Processing**: The agent worker in [`workers/agent-worker/main.ts`](https://github.com/melgarafael/DeskcommCRM/blob/main/workers/agent-worker/main.ts) handles `followup_turn` jobs via `createFollowupTurnHandler`, supporting both flow-driven and classic execution modes.
- **Retry**: Anti-ban vetoes trigger `rescheduleReentry` to create new `cron_jobs` entries with `reschedule_of` markers, enabling deterministic, crash-safe retries.
- **Observation**: Completion states are recorded via `completeFollowupTurn` in [`lib/followup/turn-bridge.ts`](https://github.com/melgarafael/DeskcommCRM/blob/main/lib/followup/turn-bridge.ts), with the `followup_enrollments` table serving as the source of truth for follow-up outcomes.

## Frequently Asked Questions

### What happens if the cron worker fails to enqueue a follow-up job?

The `cron_jobs` table retains the scheduled entry until successfully processed or manually cleared. Since the cron worker only moves jobs from `cron_jobs` to `job_queue` without deleting the original scheduling record until confirmation, a failed HTTP request or worker crash allows the next cron tick to retry the enqueue operation without data loss.

### How does the system prevent duplicate follow-up sends?

Idempotency is enforced at multiple layers. The `scheduleCronJob` helper ensures duplicate scheduling calls do not create multiple `cron_jobs` entries. Additionally, the `rescheduleReentry` function includes a `reschedule_of` marker in the payload, allowing the system to track retry chains and prevent double-sending even if the anti-ban guard triggers multiple vetoes.

### What is the difference between flow-driven and classic follow-up execution?

Flow-driven execution occurs when the payload contains a `followup_enrollment_id`, triggering the `runFlowDrivenTurn` logic that follows predefined conversation flows. Classic execution follows the standard "template or agent" path used for ad-hoc responses, providing flexibility when no structured enrollment exists.

### Where can I query the current status of pending follow-ups?

The [`lib/followup/retorno-crm.ts`](https://github.com/melgarafael/DeskcommCRM/blob/main/lib/followup/retorno-crm.ts) module provides utilities to query the `cron_jobs` table specifically for follow-up entries. This enables monitoring scripts and debugging tools to observe pending `followup_turn` jobs, their scheduled times, and any `reschedule_of` markers indicating retry chains.