Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
786a1e9
fix: small optimization, no need to query if there are no claims
mrkaye97 May 7, 2026
09b749e
feat: add idempotency key expr to proto, workflow version
mrkaye97 May 7, 2026
85c9c1f
feat: wiring
mrkaye97 May 7, 2026
912d471
fix: wiring to db
mrkaye97 May 7, 2026
247dd6c
feat: add ttl col
mrkaye97 May 7, 2026
890d513
Merge branch 'main' into mk/feat-idempotency
mrkaye97 May 29, 2026
92e3c37
chore: gen a whole bunch of python
mrkaye97 May 29, 2026
d490686
feat: wire up idempotency config on the sdk
mrkaye97 May 29, 2026
73beb46
fix: engine wiring
mrkaye97 May 29, 2026
3054857
fix: py type
mrkaye97 May 29, 2026
8214dfd
chore: revert worker changes
mrkaye97 May 29, 2026
5b432bb
feat: new example
mrkaye97 May 29, 2026
5234b9a
feat: fix migration, add cols
mrkaye97 May 29, 2026
dff7c68
feat: wiring
mrkaye97 May 29, 2026
0e6a5b0
feat: wire up idempotency to the triggerTuple
mrkaye97 May 29, 2026
30fb2fa
feat: initial engine wiring
mrkaye97 May 29, 2026
3fec245
fix: len
mrkaye97 May 29, 2026
ed2d3e6
fix: types
mrkaye97 May 29, 2026
3095268
feat: add test
mrkaye97 May 29, 2026
d079090
chore: rm unused deps
mrkaye97 May 29, 2026
67f34f8
fix: use the v1 trigger endpoint on python
mrkaye97 Jun 1, 2026
ecd2b6c
fix: make the python test do a real thing
mrkaye97 Jun 1, 2026
4e7aff9
feat: first pass bypassing mq fallback for idempotency
mrkaye97 Jun 1, 2026
222175a
fix: remove todo, return the same id
mrkaye97 Jun 1, 2026
4942ad4
feat: wire up existing run ids
mrkaye97 Jun 1, 2026
7a7e5f8
fix: use test run id to dedupe across test runs
mrkaye97 Jun 1, 2026
a61c858
fix: improve test a bit
mrkaye97 Jun 1, 2026
5089761
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jun 2, 2026
3ceb405
chore: lock
mrkaye97 Jun 2, 2026
ca5f63c
fix: add grpcio types
mrkaye97 Jun 2, 2026
0e4cafb
fix: drive by fix for a flaky test caused by event condition race
mrkaye97 Jun 2, 2026
9929b37
fix: not found behavior on the get_details method
mrkaye97 Jun 2, 2026
e5d5402
fix: revert python changes
mrkaye97 Jun 2, 2026
1c93e1c
fix: handle collisions in the v1 ingest endpoint :facepalm:
mrkaye97 Jun 2, 2026
05196fa
chore: gen
mrkaye97 Jun 2, 2026
4f3103c
chore: add back some comments to shrink diff
mrkaye97 Jun 2, 2026
71784e7
chore: python version, changelog
mrkaye97 Jun 2, 2026
cdd0f43
fix: use the correct tx
mrkaye97 Jun 2, 2026
bfc997b
fix: couple query bugs
mrkaye97 Jun 2, 2026
7bcaace
fix: couple more event changes
mrkaye97 Jun 2, 2026
4781050
feat: idempotency test with events
mrkaye97 Jun 2, 2026
3228fc7
fix: type issue
mrkaye97 Jun 2, 2026
8cc8500
fix: tests, lint
mrkaye97 Jun 2, 2026
ed36fc5
fix: test cleanup
mrkaye97 Jun 2, 2026
98823da
fix: handle deduplication in the query
mrkaye97 Jun 2, 2026
c363098
fix: test, gen
mrkaye97 Jun 2, 2026
99b4a33
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jun 3, 2026
31c4742
fix: conflict
mrkaye97 Jun 3, 2026
59f0fcf
fix: add prefix to idempotency key claim, clean up a tiny bit
mrkaye97 Jun 3, 2026
1ddb00b
fix: migration name, add idempotency key to cel eval failure source enum
mrkaye97 Jun 3, 2026
70ff89a
fix: pr comments
mrkaye97 Jun 3, 2026
5dcc913
chore: gen
mrkaye97 Jun 3, 2026
6667e58
feat: snippets, initial doc work
mrkaye97 Jun 3, 2026
f01f0de
chore: vibe code other sdks
mrkaye97 Jun 3, 2026
0bdfc1c
chore: vendor the status proto for js
mrkaye97 Jun 3, 2026
de07a78
fix: go docs
mrkaye97 Jun 3, 2026
8ec5334
fix: ruby simplification
mrkaye97 Jun 3, 2026
a9ae548
chore: changelogs
mrkaye97 Jun 3, 2026
cf744a9
fix: ruby ci
mrkaye97 Jun 3, 2026
4196bae
fix: more test fixes
mrkaye97 Jun 3, 2026
342ab25
chore: gen
mrkaye97 Jun 3, 2026
6c46e84
chore: gen again
mrkaye97 Jun 3, 2026
02df6ad
fix: tests
mrkaye97 Jun 3, 2026
be351c2
chore: gen
mrkaye97 Jun 3, 2026
d3b0a76
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jun 3, 2026
c9b5e1d
chore: changelogs
mrkaye97 Jun 4, 2026
744b56d
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jun 4, 2026
433ebfc
chore: lockfile
mrkaye97 Jun 4, 2026
82b3976
chore: lint
mrkaye97 Jun 4, 2026
f399e4e
chore: appease the cop
mrkaye97 Jun 4, 2026
21756f7
chore: fix copilot comments
mrkaye97 Jun 4, 2026
94f1c92
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jul 2, 2026
1362a02
chore: gen
mrkaye97 Jul 2, 2026
550a283
chore: migration version
mrkaye97 Jul 2, 2026
f4a4f00
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jul 8, 2026
77f8518
chore: migration version
mrkaye97 Jul 8, 2026
2ee5eaf
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jul 14, 2026
94a788b
fix: ordering to prevent deadlocks
mrkaye97 Jul 14, 2026
e8f0c0f
fix: add validation, add skeleton for handling collisions on bulk tri…
mrkaye97 Jul 14, 2026
620c9aa
feat: add key to v1_task
mrkaye97 Jul 14, 2026
bce16cd
feat: wiring
mrkaye97 Jul 14, 2026
3c354f4
feat: add idempotency keys to runs, dags, etc. tables
mrkaye97 Jul 14, 2026
e4b6744
feat: wire up sending idempotency back over the api to the dash
mrkaye97 Jul 14, 2026
5fdfcf7
feat: wire up idempotency filter and column on the fe
mrkaye97 Jul 14, 2026
08ffdf6
feat: return partial success on bulk trigger via an error
mrkaye97 Jul 14, 2026
ace9b99
feat: wrap bulk triggers
mrkaye97 Jul 14, 2026
7b1c768
feat: add test
mrkaye97 Jul 14, 2026
01c116b
feat: go migration for indexes
mrkaye97 Jul 14, 2026
9f5d882
fix: order
mrkaye97 Jul 14, 2026
4a9c153
fix: down
mrkaye97 Jul 14, 2026
be9fe2e
fix: weird diff
mrkaye97 Jul 14, 2026
095b8be
fix: always create trigger writer
mrkaye97 Jul 14, 2026
55b8d83
chore: gen other sdks
mrkaye97 Jul 14, 2026
0875ee3
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jul 14, 2026
ace6ea2
fix: queries
mrkaye97 Jul 14, 2026
a631485
fix: param ordering
mrkaye97 Jul 14, 2026
34ae8a3
chore: gen
mrkaye97 Jul 14, 2026
8bc4b0c
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jul 14, 2026
b80f1ef
chore: prettier
mrkaye97 Jul 14, 2026
1d3604d
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jul 15, 2026
e8b6858
fix: merge
mrkaye97 Jul 15, 2026
30db04f
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jul 15, 2026
2ded249
fix: migration
mrkaye97 Jul 15, 2026
16bc22f
chore: lint
mrkaye97 Jul 15, 2026
9ee643c
chore: bundle
mrkaye97 Jul 15, 2026
96ec3e8
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jul 15, 2026
b287cf2
Merge branch 'main' into mk/feat-idempotency
mrkaye97 Jul 16, 2026
9f53f05
chore: gen changelogs
mrkaye97 Jul 16, 2026
efb5a3c
feat: add TTL-based idempotency config + ABC to allow for better exte…
mrkaye97 Jul 16, 2026
b8eff31
chore: other sdks, gen
mrkaye97 Jul 16, 2026
5b367ee
chore: lint
mrkaye97 Jul 16, 2026
0379ac6
fix: rubocop
mrkaye97 Jul 16, 2026
80593aa
fix: rm generic
mrkaye97 Jul 16, 2026
1c1fbdb
chore: schema
mrkaye97 Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api-contracts/openapi/components/schemas/v1/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ V1TaskSummary:
type: string
description: The external ID of the parent task.
format: uuid
idempotencyKey:
type: string
description: The idempotency key that was claimed by the task run
required:
- metadata
- createdAt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ listWorkflowRuns:
required: false
schema:
$ref: "../../../components/schemas/_index.yaml#/V1RunningFilter"
- description: The idempotency key(s) to filter for
in: query
name: idempotency_keys
required: false
schema:
type: array
items:
type: string
responses:
"200":
content:
Expand Down
16 changes: 16 additions & 0 deletions api-contracts/v1/workflows.proto
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,24 @@ message CreateWorkflowVersionRequest {
repeated Concurrency concurrency_arr = 12; // (optional) the workflow concurrency options
repeated DefaultFilter default_filters = 13; // (optional) the default filters for the workflow
optional bytes input_json_schema = 14; // (optional) the JSON schema for the workflow input

optional IdempotencyConfig idempotency = 15; // (optional) idempotency configuration for the workflow
}

message IdempotencyConfig {
string expression = 1; // a CEL expression for determining the idempotency key for workflow runs
int64 ttl_ms = 2; // time-to-live for idempotency keys in milliseconds
}

message IdempotencyCollisionError {
string existing_run_external_id = 1; // the external ID of the existing workflow run that caused the collision
string colliding_run_external_id = 2; // the external ID of the workflow run that caused the collision
}

message BulkTriggerIdempotencyCollisionError {
repeated string successful_workflow_run_external_ids = 1; // the external IDs of the successfully triggered workflow runs
repeated IdempotencyCollisionError collisions = 2; // the idempotency collision errors
}

message DefaultFilter {
string expression = 1; // (required) the CEL expression for the filter
Expand Down
2 changes: 2 additions & 0 deletions api/v1/server/handlers/v1/workflow-runs/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func (t *V1WorkflowRunsService) WithDags(ctx context.Context, request gen.V1Work
Offset: offset,
IncludePayloads: includePayloads,
UseGinIndex: useGinIndex,
IdempotencyKeys: request.Params.IdempotencyKeys,
}

additionalMetadataFilters := make(map[string]interface{})
Expand Down Expand Up @@ -291,6 +292,7 @@ func (t *V1WorkflowRunsService) OnlyTasks(ctx context.Context, request gen.V1Wor
WorkerId: request.Params.WorkerId,
IncludePayloads: includePayloads,
UseGinIndex: useGinIndex,
IdempotencyKeys: request.Params.IdempotencyKeys,
}

additionalMetadataFilters := make(map[string]interface{})
Expand Down
460 changes: 237 additions & 223 deletions api/v1/server/oas/gen/openapi.gen.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion api/v1/server/oas/transformers/v1/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func ToV1Event(event *v1.EventWithPayload) gen.V1Event {
},
Payload: &payload,
SeenAt: &event.EventSeenAt.Time,
Scope: &event.EventScope,
Scope: event.EventScope,
TriggeredRuns: &triggeredRuns,
TriggeringWebhookName: event.TriggeringWebhookName,
}
Expand Down
8 changes: 8 additions & 0 deletions api/v1/server/oas/transformers/v1/workflow_runs.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func WorkflowRunDataToV1TaskSummary(task *v1.WorkflowRunData, workflowIdsToNames
RetryCount: &retryCount,
Attempt: &attempt,
ParentTaskExternalId: parentTaskExternalId,
IdempotencyKey: task.IdempotencyKey,
}

if isEvicted {
Expand Down Expand Up @@ -192,6 +193,12 @@ func PopulateTaskRunDataRowToV1TaskSummary(task *v1.TaskWithPayloads, workflowNa

taskStatus, isEvicted := mapOlapStatus(string(task.Status))

var idempotencyKey *string

if task.IdempotencyKey.Valid {
idempotencyKey = &task.IdempotencyKey.String
}

summary := gen.V1TaskSummary{
Metadata: gen.APIResourceMeta{
Id: task.ExternalID.String(),
Expand Down Expand Up @@ -223,6 +230,7 @@ func PopulateTaskRunDataRowToV1TaskSummary(task *v1.TaskWithPayloads, workflowNa
Attempt: &attempt,
WorkflowRunExternalId: task.WorkflowRunID,
ParentTaskExternalId: task.ParentTaskExternalID,
IdempotencyKey: idempotencyKey,
}

if isEvicted {
Expand Down
265 changes: 265 additions & 0 deletions cmd/hatchet-migrate/migrate/migrations/20260715180517_v1_0_129.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE "WorkflowVersion"
ADD COLUMN "idempotencyKeyExpression" TEXT,
ADD COLUMN "idempotencyKeyTtlMs" BIGINT
;

ALTER TYPE v1_cel_evaluation_failure_source ADD VALUE IF NOT EXISTS 'IDEMPOTENCY_KEY';

ALTER TABLE v1_task ADD COLUMN idempotency_key TEXT;
ALTER TABLE v1_dag ADD COLUMN idempotency_key TEXT;

ALTER TABLE v1_tasks_olap ADD COLUMN idempotency_key TEXT;
ALTER TABLE v1_dags_olap ADD COLUMN idempotency_key TEXT;
ALTER TABLE v1_runs_olap ADD COLUMN idempotency_key TEXT;

CREATE OR REPLACE FUNCTION v1_tasks_olap_insert_function()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO v1_runs_olap (
tenant_id,
id,
inserted_at,
external_id,
readable_status,
kind,
workflow_id,
workflow_version_id,
additional_metadata,
parent_task_external_id,
idempotency_key
)
SELECT
tenant_id,
id,
inserted_at,
external_id,
readable_status,
'TASK',
workflow_id,
workflow_version_id,
additional_metadata,
parent_task_external_id,
idempotency_key
FROM new_rows
WHERE dag_id IS NULL
ON CONFLICT (inserted_at, id) DO NOTHING;

INSERT INTO v1_lookup_table_olap (
tenant_id,
external_id,
task_id,
inserted_at
)
SELECT
tenant_id,
external_id,
id,
inserted_at
FROM new_rows
ON CONFLICT (external_id) DO NOTHING;

-- If the task has a dag_id and dag_inserted_at, insert into the lookup table
INSERT INTO v1_dag_to_task_olap (
dag_id,
dag_inserted_at,
task_id,
task_inserted_at
)
SELECT
dag_id,
dag_inserted_at,
id,
inserted_at
FROM new_rows
WHERE dag_id IS NOT NULL
ON CONFLICT (dag_id, dag_inserted_at, task_id, task_inserted_at) DO NOTHING;

RETURN NULL;
END;
$$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION v1_dags_olap_insert_function()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO v1_runs_olap (
tenant_id,
id,
inserted_at,
external_id,
readable_status,
kind,
workflow_id,
workflow_version_id,
additional_metadata,
parent_task_external_id,
idempotency_key
)
SELECT
tenant_id,
id,
inserted_at,
external_id,
readable_status,
'DAG',
workflow_id,
workflow_version_id,
additional_metadata,
parent_task_external_id,
idempotency_key
FROM new_rows
ON CONFLICT (inserted_at, id) DO NOTHING;

INSERT INTO v1_lookup_table_olap (
tenant_id,
external_id,
dag_id,
inserted_at
)
SELECT
tenant_id,
external_id,
id,
inserted_at
FROM new_rows
ON CONFLICT (external_id) DO NOTHING;

RETURN NULL;
END;
$$
LANGUAGE plpgsql;
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
ALTER TABLE "WorkflowVersion"
DROP COLUMN "idempotencyKeyExpression",
DROP COLUMN "idempotencyKeyTtlMs"
;

ALTER TABLE v1_task DROP COLUMN idempotency_key;
ALTER TABLE v1_dag DROP COLUMN idempotency_key;

ALTER TABLE v1_dags_olap DROP COLUMN idempotency_key;
ALTER TABLE v1_tasks_olap DROP COLUMN idempotency_key;
ALTER TABLE v1_runs_olap DROP COLUMN idempotency_key;

CREATE OR REPLACE FUNCTION v1_tasks_olap_insert_function()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO v1_runs_olap (
tenant_id,
id,
inserted_at,
external_id,
readable_status,
kind,
workflow_id,
workflow_version_id,
additional_metadata,
parent_task_external_id
)
SELECT
tenant_id,
id,
inserted_at,
external_id,
readable_status,
'TASK',
workflow_id,
workflow_version_id,
additional_metadata,
parent_task_external_id
FROM new_rows
WHERE dag_id IS NULL
ON CONFLICT (inserted_at, id) DO NOTHING;

INSERT INTO v1_lookup_table_olap (
tenant_id,
external_id,
task_id,
inserted_at
)
SELECT
tenant_id,
external_id,
id,
inserted_at
FROM new_rows
ON CONFLICT (external_id) DO NOTHING;

-- If the task has a dag_id and dag_inserted_at, insert into the lookup table
INSERT INTO v1_dag_to_task_olap (
dag_id,
dag_inserted_at,
task_id,
task_inserted_at
)
SELECT
dag_id,
dag_inserted_at,
id,
inserted_at
FROM new_rows
WHERE dag_id IS NOT NULL
ON CONFLICT (dag_id, dag_inserted_at, task_id, task_inserted_at) DO NOTHING;

RETURN NULL;
END;
$$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION v1_dags_olap_insert_function()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO v1_runs_olap (
tenant_id,
id,
inserted_at,
external_id,
readable_status,
kind,
workflow_id,
workflow_version_id,
additional_metadata,
parent_task_external_id
)
SELECT
tenant_id,
id,
inserted_at,
external_id,
readable_status,
'DAG',
workflow_id,
workflow_version_id,
additional_metadata,
parent_task_external_id
FROM new_rows
ON CONFLICT (inserted_at, id) DO NOTHING;

INSERT INTO v1_lookup_table_olap (
tenant_id,
external_id,
dag_id,
inserted_at
)
SELECT
tenant_id,
external_id,
id,
inserted_at
FROM new_rows
ON CONFLICT (external_id) DO NOTHING;

RETURN NULL;
END;
$$
LANGUAGE plpgsql;
-- +goose StatementEnd
Loading
Loading