Skip to content

feat: name runs with a CEL display-name expression on the workflow/task definition (#4259)#4375

Open
Mdev303 wants to merge 21 commits into
hatchet-dev:mainfrom
Mdev303:issue-4259
Open

feat: name runs with a CEL display-name expression on the workflow/task definition (#4259)#4375
Mdev303 wants to merge 21 commits into
hatchet-dev:mainfrom
Mdev303:issue-4259

Conversation

@Mdev303

@Mdev303 Mdev303 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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):

  • Reference run input with input.<key>, e.g. input.customerName.
  • Quote string literals as CEL strings, e.g. 'Acme Corp'.
  • Guard optional keys with has(), e.g. has(input.name) ? input.name : 'run'.

Levels & precedence

  • A workflow-level expression names the run — the DAG run for a multi-step workflow, or the task for a single-task workflow (the task is the run).
  • A per-task expression names an individual DAG step.
  • On a single-task workflow the precedence is: task expression → workflow expression → generated fallback.

Failures never fail the run. A malformed expression is rejected at registration (via the celworkflowrunstr validator). 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

customer_workflow = hatchet.workflow(
    name="CustomerWorkflow",
    display_name="input.customerName",   # names the run
)

@customer_workflow.task(display_name="'enrich-' + input.customerName")  # names this DAG step
def enrich(input: WorkflowInput, ctx: Context) -> None: ...

TypeScript

const customerWorkflow = hatchet.workflow({
  name: "customer-workflow",
  displayName: "input.customerName",
});

customerWorkflow.task({ name: "enrich", displayName: "'enrich-' + input.customerName", fn });

Go

wf := client.NewWorkflow("customer-workflow", hatchet.WithWorkflowDisplayName("input.customerName"))
wf.NewTask("enrich", enrichFn, hatchet.WithDisplayName("'enrich-' + input.customerName"))

Ruby

wf = hatchet.workflow(name: "CustomerWorkflow", display_name: "input.customerName")
wf.task(name: "enrich", display_name: "'enrich-' + input.customerName") { |input, ctx| ... }

Design decisions & trade-offs

  • CEL on the definition, not a literal at trigger time. One declaration names every run of the workflow across all trigger sources (manual/bulk/child/event/cron), and enables independent per-step names — neither of which the trigger-time literal could do.
  • Pure CEL (whole string). Consistent with concurrency and rate-limit keys; no bespoke {{ }} templating to learn or maintain.
  • Input-only environment (input, additional_metadata, workflow_run_id) — no parent-step outputs, keeping evaluation available at trigger time.
  • Validated at registration, silent at runtime. A bad expression is caught when you register the workflow (celworkflowrunstr); a runtime failure (missing key / non-string / empty) never fails the run — it falls back to the generated name. >255 runes → truncated (rune-safe), never rejected.
  • Versioned. The expressions are folded into the workflow-version checksum, so changing an expression produces a new version.
  • Backward compatible. Both display_name fields are optional; omitting them reproduces today's exact generated-name behavior.

What's changed

  • Contracts: removed the trigger-time display_name (TriggerWorkflowRequest Feat: introduce a scheduling state #11, TriggerWorkflowRunRequest chore(contributing): adapt deps, hardcode generator versions #6) and marked both field numbers/names reserved; added CreateWorkflowVersionRequest.display_name (chore(frontend): setup eslint and prettier with better defaults #15) and CreateTaskOpts.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.
  • Storage: nullable displayName columns on WorkflowVersion and Step (migration 20260721120000_v1_0_126) + sqlc regen; opts tagged celworkflowrunstr and folded into the version checksum.
  • Engine eval: pure resolveDisplayName helper; workflow-level expr evaluated in createDAGsv1_dag.display_name, step/workflow expr evaluated in insertTasksv1_task.display_name, with silent fallback + NormalizeDisplayName (trim + 255-rune truncate).
  • admin service: req.DisplayName / step DisplayName threaded through getCreateWorkflowOpts / getCreateTaskOpts so definition expressions reach the repository via normal gRPC PutWorkflow.
  • SDKs (Go, TypeScript, Python, Ruby): definition-level display_name on the workflow and task builders (mirroring concurrency), threaded into CreateWorkflowVersionRequest / CreateTaskOpts; all trigger-time run-call surfaces removed.
  • Docs / changelog: run-names.mdx rewritten for the CEL mechanism; root + per-SDK changelogs updated.

Type of change

  • New feature (non-breaking change which adds functionality)

Checklist

  • TestedresolveDisplayName unit 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).
  • Linted / type-checked (Go vet, tsc + eslint, mypy + ruff, Ruby specs).
  • Documented (Run Names guide rewritten).
  • Added to CHANGELOG (root + per-SDK).
🤖 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

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>
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

@Mdev303 is attempting to deploy a commit to the Hatchet Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added documentation Improvements or additions to documentation sdk-ts Related to the Typescript SDK sdk-go Related to the Go SDK sdk-py Related to the Python sdk engine Related to the core Hatchet engine sdk-ruby Related to the Ruby SDK dashboard Related to the Hatchet dashboard labels Jul 8, 2026
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>
@Mdev303
Mdev303 marked this pull request as draft July 8, 2026 01:54
Mdev303 and others added 3 commits July 8, 2026 03:57
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>
@Mdev303
Mdev303 marked this pull request as ready for review July 8, 2026 02:28
@Mdev303

Mdev303 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

I will fix the conflicts once I get a review , it's mostly changelog stuff or the proto regen

@abelanger5

Copy link
Copy Markdown
Contributor

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:

  1. It would allow us to set display names on specific steps more easily
  2. It would allow for display names when triggering from events or dynamically registered crons
  3. It's consistent with how other configuration is written on a per-step basis; priority is one of the exceptions, but even that has a per-step default.

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 displayName on the input.

Mdev303 and others added 3 commits July 21, 2026 16:57
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>
@github-actions github-actions Bot removed the dashboard Related to the Hatchet dashboard label Jul 21, 2026
@Mdev303 Mdev303 changed the title feat: dynamic display_name for triggered runs (#4259) feat: name runs with a CEL display-name expression on the workflow/task definition (#4259) Jul 21, 2026
Mdev303 and others added 2 commits July 21, 2026 22:08
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>
@Mdev303
Mdev303 marked this pull request as draft July 21, 2026 20:29
Mdev303 and others added 4 commits July 21, 2026 22:46
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>
@Mdev303
Mdev303 marked this pull request as ready for review July 21, 2026 23:00
@Mdev303

Mdev303 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@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

image

let me know if there is anything else

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation engine Related to the core Hatchet engine sdk-go Related to the Go SDK sdk-py Related to the Python sdk sdk-ruby Related to the Ruby SDK sdk-ts Related to the Typescript SDK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] dynamic names for hatchet tasks

2 participants