Skip to main content

Overview

Both @durable-effect/workflow and @durable-effect/jobs emit structured events at each meaningful lifecycle point. These events can be delivered to an external HTTP endpoint for observability, discarded silently, or captured in-memory during tests. The core abstraction is the EventTracker Effect service. The engine emits events through it; the implementation decides what to do with them.

EventTracker service

class EventTracker extends Context.Tag("@durable-effect/EventTracker")<
  EventTracker,
  EventTrackerService
>() {}

interface EventTrackerService<E extends BaseTrackingEvent = BaseTrackingEvent> {
  readonly emit: (event: E) => Effect<void>
  readonly flush: () => Effect<void>
  readonly pending: () => Effect<number>
}
Three helper functions let you interact with the tracker safely from any Effect — they do nothing if the tracker is not in scope:
emitEvent(event)   // Buffer an event
flushEvents        // Flush the buffer
getPendingEvents   // Get buffered count

Workflow events

The following events are emitted during workflow execution:
Event typeWhen emitted
workflow.startedWorkflow begins execution
workflow.completedWorkflow finishes successfully
workflow.failedWorkflow fails with an error
workflow.pausedWorkflow pauses (sleep or retry delay)
workflow.resumedWorkflow resumes after a pause
step.startedA step begins executing
step.completedA step finishes successfully
step.failedA step fails
retry.scheduledA retry delay is scheduled
retry.exhaustedAll retry attempts have been used
sleep.startedA sleep begins
sleep.completedA sleep finishes (workflow resumes)
The library uses two sets of schemas for each event type:
  • Internal schemas (InternalWorkflowEventSchema, etc.) — used by the library’s own code to construct events.
  • Wire schemas (WorkflowStartedEventSchema, etc.) — the shape sent to your external endpoint after enrichment with env and serviceKey.
Both sets are exported from @durable-effect/core if you need to validate or parse events on the receiving end.

Job events

The following events are emitted during job execution:
Event typeWhen emitted
job.startedJob instance is created
job.executedExecute function completes successfully
job.failedExecute function throws an error
job.retryExhaustedAll retry attempts have been used
job.terminatedJob instance is terminated and state purged
debounce.startedFirst event is added to a debounce job
debounce.flushedA debounce batch is processed
task.scheduledExecution is scheduled for a task job

Configuring HTTP batch delivery

Pass a tracker object when creating the engine. Events are buffered and sent as a JSON batch via HTTP POST. For workflows:
import { createDurableWorkflows } from "@durable-effect/workflow";

export const { Workflows, WorkflowClient } = createDurableWorkflows(workflows, {
  tracker: {
    endpoint: "https://events.example.com/ingest",
    env: "production",
    serviceKey: "my-app",
  },
});
For jobs:
import { createDurableJobs } from "@durable-effect/jobs";

const { Jobs, JobsClient } = createDurableJobs(
  { tokenRefresher, webhookBatcher },
  {
    tracker: {
      endpoint: "https://events.example.com/ingest",
      env: "production",
      serviceKey: "my-jobs-service",
    },
  }
);

Full HttpBatchTrackerConfig

interface HttpBatchTrackerConfig {
  /** URL to POST events to */
  endpoint: string
  /** Identifies the environment in every event payload */
  env: string
  /** Identifies the service in every event payload */
  serviceKey: string
  /** Maximum events per batch (default: 100) */
  batchSize?: number
  /** Extra headers included in every request */
  headers?: Record<string, string>
  retry?: {
    /** Maximum delivery attempts (default: 3) */
    maxAttempts?: number
    /** Initial delay in ms for exponential backoff (default: 1000) */
    initialDelayMs?: number
  }
}
The tracker enriches each event with env and serviceKey before sending. Events are batched up to batchSize and flushed automatically. If delivery fails, the tracker retries with exponential backoff and then silently drops the batch rather than failing the workflow.

HttpBatchTrackerLayer

If you are composing layers manually outside of the engine factory, use HttpBatchTrackerLayer directly:
import { HttpBatchTrackerLayer } from "@durable-effect/core";

const trackerLayer = HttpBatchTrackerLayer({
  endpoint: "https://events.example.com/ingest",
  env: "production",
  serviceKey: "my-app",
});

NoopTrackerLayer

When no tracker config is provided, the engine defaults to NoopTrackerLayer. It implements the EventTracker interface but discards every event:
import { NoopTrackerLayer } from "@durable-effect/core";

// No events are recorded or sent
const app = pipe(myEffect, Effect.provide(NoopTrackerLayer));

In-memory tracker for testing

Use createInMemoryTracker or createInMemoryTrackerLayer in tests to capture and assert on emitted events without any HTTP calls.
import { createInMemoryTrackerLayer } from "@durable-effect/core";

const { layer, handle } = await Effect.runPromise(
  createInMemoryTrackerLayer()
);

InMemoryTrackerHandle

interface InMemoryTrackerHandle<E extends BaseTrackingEvent = BaseTrackingEvent> {
  /** Get all recorded events */
  getEvents(): Effect<E[]>

  /** Get events filtered by type */
  getEventsByType<T extends string>(type: T): Effect<Array<Extract<E, { type: T }>>>

  /** Check whether a specific event type was emitted */
  hasEvent(type: string): Effect<boolean>

  /** Clear all recorded events */
  clear(): Effect<void>
}
import { Effect } from "effect";
import { createInMemoryTrackerLayer } from "@durable-effect/core";

const { layer, handle } = await Effect.runPromise(
  createInMemoryTrackerLayer()
);

// Run your workflow under the tracker layer
// ...

// Assert a specific event was emitted
const started = await Effect.runPromise(
  handle.hasEvent("workflow.started")
);

// Inspect all events
const events = await Effect.runPromise(handle.getEvents());

// Filter by type
const stepEvents = await Effect.runPromise(
  handle.getEventsByType("step.completed")
);

Event schema types

All schemas are exported from @durable-effect/core. There are two categories:
  • Internal types (e.g., InternalWorkflowStartedEvent) — the shape used internally by library code when constructing events.
  • Wire types (e.g., WorkflowStartedEvent) — the enriched shape sent to your endpoint, including env and serviceKey.
Import what you need for validation on the receiving service:
import {
  TrackingEventSchema,       // Combined wire schema (workflows + jobs)
  WorkflowEventSchema,       // Wire workflow events only
  JobEventSchema,            // Wire job events only
  type WorkflowStartedEvent,
  type StepCompletedEvent,
  type JobExecutedEvent,
} from "@durable-effect/core";

Build docs developers (and LLMs) love