Persistent, immutable log of every workflow execution stored in a PostgreSQL workflow_executions table: execution_id (UUID v4), workflow_id, trigger_source (webhook/schedule/manual), trigger_payload (JSONB with configurable sensitive-field masking), status (pending/running/completed/failed/timed_out), started_at, completed_at, and duration_ms. A corresponding workflow_execution_steps table records every step: step_id, execution_id (FK), step_name, step_type (action/condition/transform/wait), input_payload (JSONB), output_payload (JSONB), error_message, started_at, completed_at, duration_ms, and external_request_id for correlating with downstream system logs. The workflow_executions table is granted no UPDATE or DELETE permissions on the application database role -- immutability enforced at the database layer, not by convention.
Payload storage: JSONB columns hold complete step I/O up to 1MB per field without truncation. Payloads exceeding 1MB (bulk record updates, large document processing) are written to S3, with the JSONB column storing the S3 URI reference. Step payloads are compressed with zstd at the application layer before storage -- text-heavy JSON payloads typically compress 4-8x. Retention policy: hot storage in PostgreSQL for 90 days; automated S3 lifecycle rule moves executions older than 90 days to S3 Standard-IA; executions older than 365 days archive to S3 Glacier Instant Retrieval. Retention periods are configurable per workflow based on the regulated retention requirement of the underlying business process.
Search and investigation: execution records indexed on (workflow_id, created_at DESC) for time-range queries, (trigger_source, status) for filtered operational queries, and a GIN index on trigger_payload for finding executions by embedded field values -- customer ID, order number, invoice reference -- without full table scans. For free-text search across payload content, step input/output payloads are streamed to Elasticsearch 8.x or OpenSearch 2.x via a change-data-capture pipeline; the relational store handles operational queries and the search index handles investigation queries.
OpenTelemetry trace format: every execution carries a trace_id (W3C Trace Context 16-byte hex), span_id per step, and parent_span_id linking each step to its predecessor. Traces are exported to Jaeger, Zipkin, Datadog APM, or any OTel-compatible backend so workflow execution traces appear alongside application traces in a single observability platform. The full execution trace for any run is reconstructable from stored spans without a live tracing backend -- the data exists in the database even if the tracing backend is unavailable. The investigation UI renders each step as a horizontal timeline bar proportional to its duration, with status colour-coding (green/amber/red) and inline error annotations so the bottleneck step is visible in 3 seconds rather than requiring a log scan.