Describe the issue
The engine accepts 42m30s during workflow registration, stores that value, then enforces it as roughly 42s.
The mismatch is in convert_duration_to_interval(duration text). It extracts the leading integer and chooses the unit from the final suffix, so 42m30s becomes 42 seconds.
Environment
- SDK:
@hatchet-dev/typescript-sdk@1.17.2
- Latest SDK checked:
@hatchet-dev/typescript-sdk@1.22.1, published 2026-05-05
- Engine: self-hosted
ghcr.io/hatchet-dev/hatchet/hatchet-lite:latest
- Local engine image digest:
sha256:fd87ee914ad64845557b2f6130c749fbe99e132c568e1dac1ec2e49b0cbfaa96
Expected behavior
If registration accepts 42m30s, runtime enforcement should apply a timeout of 42 minutes and 30 seconds.
If multi-unit strings are not supported, registration should reject them instead of storing a value that runtime enforcement parses differently.
Code to Reproduce, Logs, or Screenshots
import { Hatchet } from "@hatchet-dev/typescript-sdk";
const hatchet = Hatchet.init();
const task = hatchet.task({
name: "multi-unit-timeout-repro",
executionTimeout: 2_550_000,
fn: async () => {
await new Promise((resolve) => setTimeout(resolve, 120_000));
return { ok: true };
},
});
2_550_000 ms serializes to 42m30s. The task is cancelled after roughly 43 seconds, even though the configured timeout is 42m30s.
Event log from the failed task run:
[
{
"eventType": "TIMED_OUT",
"message": "Task exceeded timeout of 42m30s",
"timestamp": "2026-04-30T09:31:59.157Z"
},
{
"eventType": "STARTED",
"timestamp": "2026-04-30T09:31:16.302Z"
}
]
The event timestamps are 42.86 seconds apart, not 42m30s.
The runtime SQL helper is where the stored string is converted to an interval for enforcement. A direct query shows the enforced interval:
SELECT
convert_duration_to_interval('11m') AS eleven_m,
convert_duration_to_interval('42m30s') AS forty_two_m_thirty_s,
convert_duration_to_interval('1h30m5s') AS one_h_thirty_m_five_s;
eleven_m | forty_two_m_thirty_s | one_h_thirty_m_five_s
----------+----------------------+-----------------------
00:11:00 | 00:00:42 | 00:00:01
Single-unit strings work as expected. For example, 660_000 ms serializes to 11m and is enforced as 11 minutes.
Re-verification (2026-05-12)
Pulled ghcr.io/hatchet-dev/hatchet/hatchet-lite:latest today, digest sha256:9511a4175d6d229ba5c14e8ed79f5ef4c458c86d2b2f7b3f8a6a0ed3f0350b7e. The image was built on 2026-04-30T15:21:28Z, so :latest has not been rebuilt since the original observation.
Running the same query against a postgres migrated by the image's bundled /hatchet-migrate returns the same result:
eleven_m | forty_two_m_thirty_s | one_h_thirty_m_five_s
----------+----------------------+-----------------------
00:11:00 | 00:00:42 | 00:00:01
(1 row)
Additional context
The TypeScript SDK accepts and emits multi-unit duration strings:
type TwoUnitDurations = `${number}h${number}m` | `${number}h${number}s` | `${number}m${number}s`;
type ThreeUnitDurations = `${number}h${number}m${number}s`;
Numeric durations are normalized to the same Go-style string format. For example, 2_550_000 ms becomes 42m30s.
Registration validation accepts these strings because the duration validator uses Go's time.ParseDuration, which accepts values such as 42m30s and 1h30m5s.
The public timeout docs describe a single-unit format, such as 10s, 4m, and 1h, but the TypeScript SDK type/tests and engine validation accept multi-unit values.
The current SQL helper does not enforce the same grammar:
num_value := substring(duration from '^\d+');
RETURN CASE
WHEN duration LIKE '%ms' THEN make_interval(secs => num_value::float / 1000)
WHEN duration LIKE '%s' THEN make_interval(secs => num_value)
WHEN duration LIKE '%m' THEN make_interval(mins => num_value)
WHEN duration LIKE '%h' THEN make_interval(hours => num_value)
WHEN duration LIKE '%d' THEN make_interval(days => num_value)
WHEN duration LIKE '%w' THEN make_interval(days => num_value * 7)
WHEN duration LIKE '%y' THEN make_interval(months => num_value * 12)
ELSE '5 minutes'::interval
END;
Because that helper is reused, affected surfaces include execution timeouts, schedule timeouts, ctx.refreshTimeout, durable sleeps, onFailure task timeouts, and onSuccess task timeouts.
An engine-side fix is needed because existing stored values and non-TypeScript clients can still send multi-unit strings. Runtime enforcement should match registration validation, or registration should reject values that runtime enforcement cannot parse correctly.
Describe the issue
The engine accepts
42m30sduring workflow registration, stores that value, then enforces it as roughly42s.The mismatch is in
convert_duration_to_interval(duration text). It extracts the leading integer and chooses the unit from the final suffix, so42m30sbecomes42 seconds.Environment
@hatchet-dev/typescript-sdk@1.17.2@hatchet-dev/typescript-sdk@1.22.1, published 2026-05-05ghcr.io/hatchet-dev/hatchet/hatchet-lite:latestsha256:fd87ee914ad64845557b2f6130c749fbe99e132c568e1dac1ec2e49b0cbfaa96Expected behavior
If registration accepts
42m30s, runtime enforcement should apply a timeout of 42 minutes and 30 seconds.If multi-unit strings are not supported, registration should reject them instead of storing a value that runtime enforcement parses differently.
Code to Reproduce, Logs, or Screenshots
2_550_000ms serializes to42m30s. The task is cancelled after roughly 43 seconds, even though the configured timeout is42m30s.Event log from the failed task run:
[ { "eventType": "TIMED_OUT", "message": "Task exceeded timeout of 42m30s", "timestamp": "2026-04-30T09:31:59.157Z" }, { "eventType": "STARTED", "timestamp": "2026-04-30T09:31:16.302Z" } ]The event timestamps are 42.86 seconds apart, not
42m30s.The runtime SQL helper is where the stored string is converted to an interval for enforcement. A direct query shows the enforced interval:
Single-unit strings work as expected. For example,
660_000ms serializes to11mand is enforced as 11 minutes.Re-verification (2026-05-12)
Pulled
ghcr.io/hatchet-dev/hatchet/hatchet-lite:latesttoday, digestsha256:9511a4175d6d229ba5c14e8ed79f5ef4c458c86d2b2f7b3f8a6a0ed3f0350b7e. The image was built on 2026-04-30T15:21:28Z, so:latesthas not been rebuilt since the original observation.Running the same query against a postgres migrated by the image's bundled
/hatchet-migratereturns the same result:Additional context
The TypeScript SDK accepts and emits multi-unit duration strings:
Numeric durations are normalized to the same Go-style string format. For example,
2_550_000ms becomes42m30s.Registration validation accepts these strings because the
durationvalidator uses Go'stime.ParseDuration, which accepts values such as42m30sand1h30m5s.The public timeout docs describe a single-unit format, such as
10s,4m, and1h, but the TypeScript SDK type/tests and engine validation accept multi-unit values.The current SQL helper does not enforce the same grammar:
Because that helper is reused, affected surfaces include execution timeouts, schedule timeouts,
ctx.refreshTimeout, durable sleeps,onFailuretask timeouts, andonSuccesstask timeouts.An engine-side fix is needed because existing stored values and non-TypeScript clients can still send multi-unit strings. Runtime enforcement should match registration validation, or registration should reject values that runtime enforcement cannot parse correctly.