Heartbeat Execution Flow

Full execution sequence: trigger fires → eligible agents selected → adapter.invoke() called → status polled → result written to heartbeat_runs → cost_event recorded → activity logged.

Heartbeat Execution Flow: Code Walkthrough

What you'll learn

Follow the heartbeat source code from the cron trigger through the adapter call to the database write, understanding each layer's responsibility.

Entry Point

packages/server/src/lib/heartbeat/
├── scheduler.ts    ← registers the cron
├── runner.ts       ← orchestrates one tick
├── dispatcher.ts   ← assigns issues to agents
└── recorder.ts     ← writes runs and cost events

Step 1 — scheduler.ts

export async function startHeartbeatScheduler() {
  const companies = await db.companies.findAll({ where: { paused: false } });
  for (const company of companies) {
    const expr = company.heartbeat_cron ?? DEFAULT_CRON;
    cron.schedule(expr, () => runHeartbeatTick(company.id));
  }
}

Runs once at server startup; schedules one cron job per active company.

Step 2 — runner.ts

export async function runHeartbeatTick(companyId: string) {
  // 1. Create the run record
  const run = await db.heartbeat_runs.create({
    company_id: companyId,
    status: 'running',
    started_at: new Date(),
  });

  try {
    // 2. Check company state
    const company = await db.companies.findOneOrFail(companyId);
    if (company.paused) {
      await recorder.skip(run.id, 'company_paused');
      return;
    }

    // 3. Dispatch
    const stats = await dispatcher.dispatchAll(companyId, run.id);

    // 4. Record completion
    await recorder.complete(run.id, stats);
  } catch (err) {
    await recorder.fail(run.id, err);
  }
}

Step 3 — dispatcher.ts

export async function dispatchAll(companyId: string, runId: string) {
  const agents = await db.agents.findAll({
    where: { company_id: companyId, status: 'active' },
  });

  const results = await Promise.allSettled(
    agents.map(agent => dispatchOne(agent, companyId, runId))
  );

  return aggregateStats(results);
}

async function dispatchOne(agent, companyId, runId) {
  // Atomic checkout
  const issue = await claimNextIssue(agent.id, companyId);
  if (!issue) return { claimed: 0, resolved: 0, cost: 0 };

  // Assemble prompt and invoke
  const adapter = createAdapter(agent);
  const task    = assembleTask(agent, issue);
  const result  = await adapter.invoke(task);

  // Write outcome
  await resolveIssue(issue.id, result);
  await recorder.recordCost(agent.id, companyId, runId, result.cost);

  return { claimed: 1, resolved: 1, cost: result.cost?.costUsd ?? 0 };
}

Step 4 — recorder.ts

export async function recordCost(agentId, companyId, runId, cost) {
  if (!cost) return;
  await db.cost_events.create({
    agent_id:      agentId,
    company_id:    companyId,
    heartbeat_run_id: runId,
    model:         cost.model,
    input_tokens:  cost.inputTokens,
    output_tokens: cost.outputTokens,
    cost_usd:      cost.costUsd,
  });
}

Key Architectural Choices

Promise.allSettled — agents run in parallel. If one agent's adapter throws, the others still complete. The run records partial failures.

No global transaction — each issue claim and resolution is its own transaction. This prevents one slow adapter from holding a lock on the entire company.

Idempotent run ID — the run ID is created before any work starts, so partial failures can be resumed or replayed.

Checkpoint ✓

Sign in for progress tracking & Q&A →