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

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.

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

Handler Registration in the Agent Worker

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

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:

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. 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). 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 endpoint moves scheduled entries into the active job_queue without blocking on execution.
  • Processing: The agent worker in 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, 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →