Summary
listWorkflowSteps (and the underlying getAllOperationResults) returns startedAtEpochMs / completedAtEpochMs as strings rather than numbers, because the operation_outputs.started_at_epoch_ms / completed_at_epoch_ms columns are BIGINT and node-postgres returns int8 (OID 20) as a string by default — and the step mapper passes the raw row value through without a Number() cast.
This contradicts the declared type (WorkflowStepStatus.startedAtEpochMs?: number) and is inconsistent with the workflow-status path, which does cast.
Version
@dbos-inc/dbos-sdk@4.19.8
The inconsistency
Workflow-status mapper casts (src/system_database.js ~line 199):
startedAtEpochMs: row.started_at_epoch_ms ? Number(row.started_at_epoch_ms) : undefined,
Step mapper does not (src/workflow_management.js ~line 35-36, in listWorkflowSteps):
startedAtEpochMs: step.started_at_epoch_ms, // raw BIGINT-as-string from pg
completedAtEpochMs: step.completed_at_epoch_ms,
getAllOperationResults runs SELECT * FROM operation_outputs ... and returns rows straight from the pool, so the BIGINT columns arrive as strings.
Impact / repro
Any consumer that treats the field as the documented number breaks. Concretely:
const steps = await DBOS.listWorkflowSteps(id);
new Date(steps[0].startedAtEpochMs).toISOString();
// startedAtEpochMs is "1751015751123" (string)
// new Date("1751015751123") parses it as a date STRING, not epoch ms
// => Invalid Date => RangeError: Invalid Date on .toISOString()
Steps with a NULL epoch (the common case) slip through; steps that actually have a recorded start/complete time (queued/recovered steps) trigger it.
Suggested fix
Cast in the step mapper to match getWorkflowStatus:
startedAtEpochMs: step.started_at_epoch_ms != null ? Number(step.started_at_epoch_ms) : undefined,
completedAtEpochMs: step.completed_at_epoch_ms != null ? Number(step.completed_at_epoch_ms) : undefined,
(Epoch-ms fits in a double well past year 275760, so Number() is lossless here.)
Summary
listWorkflowSteps(and the underlyinggetAllOperationResults) returnsstartedAtEpochMs/completedAtEpochMsas strings rather than numbers, because theoperation_outputs.started_at_epoch_ms/completed_at_epoch_mscolumns areBIGINTand node-postgres returnsint8(OID 20) as a string by default — and the step mapper passes the raw row value through without aNumber()cast.This contradicts the declared type (
WorkflowStepStatus.startedAtEpochMs?: number) and is inconsistent with the workflow-status path, which does cast.Version
@dbos-inc/dbos-sdk@4.19.8The inconsistency
Workflow-status mapper casts (
src/system_database.js~line 199):Step mapper does not (
src/workflow_management.js~line 35-36, inlistWorkflowSteps):getAllOperationResultsrunsSELECT * FROM operation_outputs ...and returnsrowsstraight from the pool, so theBIGINTcolumns arrive as strings.Impact / repro
Any consumer that treats the field as the documented
numberbreaks. Concretely:Steps with a NULL epoch (the common case) slip through; steps that actually have a recorded start/complete time (queued/recovered steps) trigger it.
Suggested fix
Cast in the step mapper to match
getWorkflowStatus:(Epoch-ms fits in a double well past year 275760, so
Number()is lossless here.)