Skip to main content

Stem Signals

Stem exposes lifecycle signals so instrumentation can react to publish, worker, scheduler, workflow, and control-plane events without modifying runtime code.

All signal payloads implement StemEvent and dispatch through Signal<T extends StemEvent>, giving every handler a shared event shape:

  • eventName
  • occurredAt
  • attributes

Signal Catalog

CategoryStem SignalPayload HighlightsCelery Equivalent
PublishbeforeTaskPublish, afterTaskPublishEnvelope, attempt metadata, task idbefore_task_publish, after_task_publish
Worker lifecycleworkerInit, workerReady, workerStopping, workerShutdown, workerHeartbeat, workerChildInit, workerChildShutdownWorkerInfo, optional reason/timestampsworker_init, worker_ready, worker_shutting_down, worker_shutdown, heartbeat_sent, worker_process_init/shutdown
Task lifecycletaskReceived, taskPrerun, taskPostrun, taskRetry, taskSucceeded, taskFailed, taskRevokedEnvelope, WorkerInfo, attempt, result/error contexttask_received, task_prerun, task_postrun, task_retry, task_success, task_failure, task_revoked
Workflow lifecycleworkflowRunStarted, workflowRunSuspended, workflowRunResumed, workflowRunCompleted, workflowRunFailed, workflowRunCancelledrun id, workflow name, status, optional step metadatan/a
SchedulerscheduleEntryDue, scheduleEntryDispatched, scheduleEntryFailedScheduleEntry, tick timestamp, drift, error stackbeat_scheduler_ready, beat_schedule
Control planecontrolCommandReceived, controlCommandCompletedControlCommandMessage, reply status, payload/error mapscontrol_command_sent, control_command_received

Ordering & Dispatch Semantics

  • beforeTaskPublish fires immediately before broker IO; afterTaskPublish runs once persistence succeeds.
  • taskReceived is emitted when a worker claims/dequeues a task.
  • taskPrerun fires immediately before handler invocation.
  • Execution ordering is taskReceived -> taskPrerun -> handler -> taskPostrun.
  • Worker lifecycle follows workerInit -> workerReady -> optional workerStopping -> workerShutdown.
  • Scheduler signals emit due -> dispatched/failed.
  • Dispatch is sequential and priority-aware; async callbacks are awaited.
  • Listener errors are routed to StemSignals.configure(onError: ...) and do not crash the worker loop.
  • SignalContext.cancel() stops lower-priority listeners for the current emit.

Configuration

Use StemSignals.configure or supply environment variables consumed by ObservabilityConfig.fromEnvironment:

void configureSignals() {
StemSignals.configure(
configuration: const StemSignalConfiguration(
enabled: true,
enabledSignals: {'worker-heartbeat': false},
),
);
}

Environment knobs:

  • STEM_SIGNALS_ENABLED=false disables all signals.
  • STEM_SIGNALS_DISABLED=worker-heartbeat,task-prerun disables selected ones.

Listening for Signals

lib/signals.dart
List<SignalSubscription> registerPublishSignals() {
return [
StemSignals.beforeTaskPublish.connect((payload, _) {
print('Publishing ${payload.envelope.name}');
}),
StemSignals.afterTaskPublish.connect((payload, _) {
print('Published ${payload.envelope.id}');
}),
];
}

Worker-scoped filtering is available on these convenience helpers:

  • onWorkerInit, onWorkerReady, onWorkerStopping, onWorkerShutdown
  • onWorkerHeartbeat, onWorkerChildInit, onWorkerChildShutdown
  • onTaskReceived, onTaskPrerun, onTaskPostrun, onTaskSuccess, onTaskFailure, onTaskRetry, onTaskRevoked
  • onControlCommandReceived, onControlCommandCompleted

Custom Queue Events

Signals cover runtime lifecycle hooks. For application-domain events (BullMQ QueueEvents style), use QueueEventsProducer and QueueEvents.

Adapters & Middleware

  • StemSignalEmitter builds payloads and emits signals; Stem runtime uses this same emitter internally.
  • SignalMiddleware.coordinator() forwards enqueue middleware to publish signals.
  • SignalMiddleware.worker() emits receive/prerun/failure hooks from existing worker middleware chains.
lib/signals.dart
List<Middleware> buildSignalMiddlewareForProducer() {
return [SignalMiddleware.coordinator()];
}
lib/signals.dart
List<Middleware> buildSignalMiddlewareForWorker() {
return [
SignalMiddleware.worker(
workerInfo: () => const WorkerInfo(
id: 'signals-worker',
queues: ['default'],
broadcasts: [],
),
),
];
}

Celery Comparison

CeleryStemNotes
task_prerun / task_postruntaskPrerun / taskPostrunPayload includes TaskContext and worker metadata.
worker_readyworkerReadyWorker-scoped filters available via onWorkerReady(workerId: ...).
worker_process_init/shutdownworkerChildInit / workerChildShutdownMirrors isolate pool spawn/recycle notifications.
before_task_publishbeforeTaskPublishFires before broker writes.
beat_schedulescheduleEntryDispatchedCarries scheduled vs executed timestamps plus drift duration.

Signals tied to Celery-specific pools remain out of scope.