Overview

step.sendEvent publishes an event to your workspace event bus from within a running workflow. The parent step completes immediately — it does not wait for any downstream workflow triggered by the event. This is the async counterpart to step.invoke: use step.sendEvent when you want to fan out work without blocking the current workflow.

The call is memoized: on parent retry, the event is not emitted a second time.

API shape

await step.sendEvent("spawn-enricher", {
  name: "agent/contact.enrich.requested",   // event name — dot-notation convention
  data: { contactId: "c_123", priority: "high" },
  ts: undefined,   // optional: Unix timestamp in ms — omit for immediate emission
});
// Full type signature:
sendEvent(
  id: string,
  opts: {
    name: string;
    data: unknown;
    ts?: number;   // optional Unix timestamp (ms). Future ts defers emission via BullMQ delay queue.
  }
): Promise<void>

Return value

step.sendEvent returns Promise<void>. There is no result to await — the operation is fire-and-forget from the parent workflow's perspective.

Future scheduling

When ts is set to a future Unix timestamp (milliseconds), the event emission is deferred via BullMQ's delay queue. The parent step completes immediately upon scheduling — it does not block until the future time.

const tomorrow = Date.now() + 24 * 60 * 60 * 1000;
 
await step.sendEvent("schedule-followup", {
  name: "outreach/followup.scheduled",
  data: { contactId, templateId: "follow-up-v2" },
  ts: tomorrow,   // event fires ~24h from now
});
 
// Parent workflow continues immediately after this line
await step.run("record-scheduled", () => db.outreach.markScheduled(contactId));

BullMQ stores the deferred job and emits the event at ts. If the worker restarts before ts, BullMQ's durable queue ensures the event fires on recovery — no manual retry logic needed.

Memoization

On parent retry, step.sendEvent is a no-op for an already-emitted step:

  1. The serve adapter hashes "stepId:counter" to a stable key.
  2. If completedSteps[hash] is already populated, step.sendEvent returns immediately without re-inserting to the events table.
  3. No duplicate events are emitted.

This gives the same memoization guarantee as step.run: side effects happen exactly once per workflow execution.

Common patterns

Async fan-out to parallel agents

Spawn N sub-agents without waiting for any of them:

for (const contact of contacts) {
  await step.sendEvent(`spawn-enricher-${contact.id}`, {
    name: "agent/enrich.requested",
    data: { contactId: contact.id, workflowRunId: runId },
  });
}
// All N enrichment workflows start in parallel — parent does not wait

Scheduled sub-agent

Schedule follow-up work at a specific time without running a separate cron:

const sendAt = new Date("2026-05-01T09:00:00Z").getTime();
 
await step.sendEvent("schedule-campaign-blast", {
  name: "campaign/blast.scheduled",
  data: { campaignId, segmentId },
  ts: sendAt,
});

Chain without coupling

Trigger a downstream workflow by name, allowing you to evolve each workflow independently:

const { data: enriched } = await step.run("enrich", () => enrichContact(contactId));
 
// Hand off to a separate "scoring" workflow — no direct import required
await step.sendEvent("trigger-scoring", {
  name: "scoring/contact.ready",
  data: enriched,
});

Limits

ConstraintValue
Workspace scopeEvents are workspace-scoped — no cross-workspace emit
Deferred sendEvent durabilityBullMQ delay queue — durable across worker restarts
Deferred sendEvent idempotencyNOT idempotent across worker crashes before the delay job is persisted (Phase 11 follow-up)
ts precisionMillisecond Unix timestamp — use Date.now() + offset
Warning

Deferred sendEvent is not yet idempotent across worker crashes. If the worker crashes between the serve adapter call and the BullMQ add(delay:) call, the event may not be scheduled. Phase 11 (idempotency keys) closes this window. For critical scheduled events, use a dedicated step.sleep + step.sendEvent sequence instead — the sleep memoizes durably and the sendEvent fires after resume.