feat: name runs with a CEL display-name expression on the workflow/task definition (#4259)#4375
feat: name runs with a CEL display-name expression on the workflow/task definition (#4259)#4375Mdev303 wants to merge 21 commits into
Conversation
Let callers set a human-readable label on a run at trigger time, so fanned-out child/bulk runs are distinguishable in the dashboard instead of all sharing the generated <readableId>-<timestamp> name. Engine (pkg/repository): - NormalizeDisplayName: trim; empty/whitespace -> unset (generated fallback); truncate to 255 runes (UTF-8/rune-safe). Applied at both SDK and REST ingestion. - Single-task runs -> v1_task.display_name; multi-step DAGs -> v1_dag.display_name (DAG step tasks keep generated names). Excluded from all dedup/idempotency keys, so durable/child re-triggers reuse the first name (first-name-wins). REST: display_name on the trigger endpoint + admin-path normalization. Proto/OpenAPI: display_name added (trigger.proto hatchet-dev#11, workflows.proto hatchet-dev#6) + regen. SDKs (Python, TypeScript, Go, Ruby): display_name on every run surface (run / run_no_wait / aio variants, bulk & run_many per-item, child spawns). Python: carried through _create_workflow_run_request so the value actually reaches gRPC on the real run path. Docs: new "Run Names" guide + cross-links; CHANGELOG entry. Tests: engine unit (NormalizeDisplayName, idempotency-key invariance) + integration (single/DAG/fallback/whitespace/255-rune/run_many/first-name-wins), REST handler, and per-SDK request-builder tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@Mdev303 is attempting to deploy a commit to the Hatchet Team on Vercel. A member of the Team first needs to authorize it. |
Resolve conflicts for hatchet-dev#4375 (dynamic display_name for triggered runs): - workflow_run.yaml: keep both new fields (displayName + return_only_id). - openapi.gen.go: regenerated from merged spec (embedded swagger blob + DisplayName type). - data-contracts.ts: keep both displayName and return_only_id. - 12 *.pb.go: take main's protoc v5.29.6 header, keep merged body (display_name intact). - CHANGELOG.md / sdks/python/CHANGELOG.md: keep both Unreleased entries + main's releases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ay_name) - Run prettier over condition.ts / timestamp.ts, which were regenerated with protoc v7.35.1 (double quotes) but missed the prettier:fix pass, unlike the other v7.35.1-regenerated protoc files. Fixes prettier:check. - Document the new `display_name` param in the 9 run/run_no_wait/create_bulk_run_item docstrings. Fixes pydoclint DOC101/DOC103. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Revert condition.ts / timestamp.ts to origin/main: their only changes were unrelated protoc v3.19.1->v7.35.1 regeneration churn, not part of the display_name feature. Reverting keeps the PR to just the files that must change (trigger.ts / workflows.ts). Verified tsc + lint:check still pass. - Prettier-fix the child-spawning.mdx Callout block added by this branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # api/v1/server/oas/gen/openapi.gen.go # sdks/python/CHANGELOG.md
|
I will fix the conflicts once I get a review , it's mostly changelog stuff or the proto regen |
|
Hi @Mdev303, thanks for the PR! I think it would be cleaner to use CEL-based templating instead of trigger-based arguments, for the following reasons:
I'm not strictly opposed to trigger-based overrides, but I think I prefer CEL-based for this initial scope. Trigger-based is always possible by just setting a |
hatchet-dev#4259) Restores and updates the design spec (was lost when left as an ignored, untracked file). Closes the three Codex adversarial-review findings: raw run-input shape in createDAGs, event-trigger path wiring (ListWorkflowsForEvents / prepareTriggerFromEvents), and single-step workflow-level fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e workflow/task definition Pivots the display-name mechanism away from the trigger-time literal introduced in 45b2424. Instead of passing a literal display_name on every run/spawn call, a CEL expression is now declared once on the workflow and/or task DEFINITION and evaluated by the engine against each run's input at trigger time. This lets a single definition name every run — manual, run_many, child spawn, event, and cron — with no per-trigger configuration, and enables independent per-step names on a DAG. Precedence on a single-task workflow is step -> workflow -> generated `<readableId|name>-<timestamp>` fallback. Engine: - Proto: remove trigger-time display_name (TriggerWorkflowRequest hatchet-dev#11, TriggerWorkflowRunRequest hatchet-dev#6) and mark both field numbers/names `reserved`; add CreateWorkflowVersionRequest.display_name (hatchet-dev#15) and CreateTaskOpts.display_name (hatchet-dev#16). Regenerate Go stubs (protoc 29.6). - Storage: nullable `displayName` columns on WorkflowVersion and Step (migration 20260721120000_v1_0_126) + sqlc regen; opts validated with the `celworkflowrunstr` tag at registration and folded into the version checksum. - Eval: extract pure helper resolveDisplayName(); evaluate the workflow-level expr in createDAGs -> v1_dag.display_name and the step/workflow expr in insertTasks -> v1_task.display_name, with silent fallback on any eval error, missing key, non-string, or empty result. NormalizeDisplayName (trim + 255-rune truncate) applied to the evaluated output. - admin service: thread req.DisplayName / step.DisplayName through getCreateWorkflowOpts / getCreateTaskOpts so definition expressions reach the repository via normal gRPC PutWorkflow registration. SDKs (Go, TypeScript, Python, Ruby): - Add a definition-level display_name option on the workflow and task builders, mirroring the concurrency option, threaded into CreateWorkflowVersionRequest / CreateTaskOpts. Regenerate each SDK's proto stubs. - Remove all trigger-time display_name run-call surfaces (run/run_many/spawn options and the trigger option types). Docs/changelog: rewrite run-names.mdx for the CEL mechanism; update root and per-SDK changelogs. Tests: resolveDisplayName unit tests, checksum cases, admin-service conversion regression tests, per-SDK definition-threading tests, and a 13-case CEL integration suite (single-task/DAG, step-vs-workflow precedence, event triggers, fan-out distinct names, fallbacks, truncation, invalid-expr rejection). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The design spec under .context/ (ignored via .git/info/exclude) was force-added for durability. It is a working design doc, not a shippable artifact, and should not be part of the PR. Untrack it so the final tree and Files-Changed diff carry no .context/ file; the doc stays on disk as a normal ignored file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "applies to every trigger source / no configuration needed" point was stated four times and the fan-out motivation twice. Consolidate to one statement each; no facts, links, or examples removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve conflicts between the CEL display-name feature and main's new idempotency feature — the two touched the same proto messages, SQL schema, repository opts, and SDK builders. Both are kept everywhere; the only real collision was proto field 15 in CreateWorkflowVersionRequest (main took it for `idempotency`), so `display_name` was renumbered to field 16. Generated code (Go/Python/Ruby proto stubs, sqlc, TS workflows.ts) was regenerated from the resolved sources rather than hand-merged. Verified: both Go modules build, go vet clean, admin+repository display-name tests pass, TS tsc/eslint clean + 4 tests, Python 5 tests, Ruby 7 specs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ts: revert timestamp.ts / condition.ts / shared trigger.ts to origin/main.
A prior regen left them with protoc-banner + double-quote drift (their
protos didn't change, or changed only via a `reserved` field that emits
no TS), which failed the SDK prettier:check.
- python: rebuild workflows_pb2.pyi from origin/main + the display_name
field only. My local protobuf emitted `is_durable/is_evicted:
_Optional[bool]` in __init__ where CI emits `bool`, tripping the
"generated bindings out of date" check. The .py descriptor is unchanged.
- frontend: run docs prettier (3.9.5) on run-names.mdx — a long
hatchet.workflow<...>({ ... }) line needed wrapping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dup migration) - integration: main's idempotency feature changed TriggerFromWorkflowNames to return 5 values (added IdempotencyCollision + CELEvaluationFailure); update the 3 call sites in trigger_display_name_integration_test.go so the package compiles under -tags integration. - python codegen: regenerate workflows_pb2 / shared/trigger_pb2 (.py + .pyi) with the toolchain CI actually uses — grpcio-tools 1.76.0 (libprotoc 31.1, protobuf banner 6.31.1), run on Linux via Docker. My earlier local regen used a macOS wheel that emitted banner 6.33.5 and `_Optional[bool]` in __init__ stubs, and my hand-edited .pyi dropped DISPLAY_NAME_FIELD_NUMBER; all now match `generate.sh` output. - migration: rename 20260721120000_v1_0_126.sql -> _v1_0_132.sql. v1_0_126 collided with the existing 20260713150000_v1_0_126.sql; 132 is the next free number after main's 131. The goose version prefix is unchanged, so ordering/state is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@abelanger5 Thanks for the review ! I made the change it now uses CEL based templating ( I also updated the PR title and description ) I also tested with python and TS sdk manually
let me know if there is anything else |

Name runs with a human-readable label by declaring a CEL expression on the workflow and/or task definition. The engine evaluates it against each run's input at trigger time and stores the result as the run/step display name. Because the expression lives in the definition, every trigger source — manual,
run_many/bulk, child spawn, event, and cron — produces meaningful names automatically, and DAG steps can be named independently.Fixes #4259
How it works
The whole string is evaluated as one CEL expression (consistent with concurrency and rate-limit keys — there is no
{{ }}templating):input.<key>, e.g.input.customerName.'Acme Corp'.has(), e.g.has(input.name) ? input.name : 'run'.Levels & precedence
Failures never fail the run. A malformed expression is rejected at registration (via the
celworkflowrunstrvalidator). At run time, any evaluation error — missing key, non-string result, or empty result — silently falls back to the generated<readableId|name>-<timestamp>label. A result longer than 255 characters is stored truncated (rune-safe), never rejected.Usage
Set the expression on the workflow and/or task definition — it applies to every run regardless of trigger source.
Python
TypeScript
Go
Ruby
Design decisions & trade-offs
{{ }}templating to learn or maintain.input,additional_metadata,workflow_run_id) — no parent-step outputs, keeping evaluation available at trigger time.celworkflowrunstr); a runtime failure (missing key / non-string / empty) never fails the run — it falls back to the generated name.>255runes → truncated (rune-safe), never rejected.display_namefields are optional; omitting them reproduces today's exact generated-name behavior.What's changed
display_name(TriggerWorkflowRequestFeat: introduce a scheduling state #11,TriggerWorkflowRunRequestchore(contributing): adapt deps, hardcode generator versions #6) and marked both field numbers/namesreserved; addedCreateWorkflowVersionRequest.display_name(chore(frontend): setup eslint and prettier with better defaults #15) andCreateTaskOpts.display_name(chore: add docker builds #16). OpenAPI trigger body field removed. All generated code (Go, REST, frontend, and all 4 SDK proto stubs) regenerated.displayNamecolumns onWorkflowVersionandStep(migration20260721120000_v1_0_126) + sqlc regen; opts taggedcelworkflowrunstrand folded into the version checksum.resolveDisplayNamehelper; workflow-level expr evaluated increateDAGs→v1_dag.display_name, step/workflow expr evaluated ininsertTasks→v1_task.display_name, with silent fallback +NormalizeDisplayName(trim + 255-rune truncate).req.DisplayName/ stepDisplayNamethreaded throughgetCreateWorkflowOpts/getCreateTaskOptsso definition expressions reach the repository via normal gRPCPutWorkflow.display_nameon the workflow and task builders (mirroringconcurrency), threaded intoCreateWorkflowVersionRequest/CreateTaskOpts; all trigger-time run-call surfaces removed.run-names.mdxrewritten for the CEL mechanism; root + per-SDK changelogs updated.Type of change
Checklist
resolveDisplayNameunit tests, checksum cases, admin-service conversion regression tests (getCreateWorkflowOpts/getCreateTaskOpts), per-SDK definition-threading tests (Go 4, TS 9, Python 5, Ruby 7), and a 13-case CEL integration suite run against live Postgres + RabbitMQ (single-task/DAG, step-vs-workflow precedence, event-triggered runs, fan-out distinct names, fallbacks, 255-rune truncation, invalid-expr rejection).tsc+ eslint,mypy+ ruff, Ruby specs).🤖 AI Disclosure
I acknowledge that an LLM was used in the creation of this Pull Request, in accordance with Hatchet's AI_POLICY.md.
Details: Implemented with Claude Code reviewed by me