You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Activity inputs should be expression-evaluated before scheduling activities #462
The expression is then evaluated by the activity implementation rather than by the workflow before the activity is scheduled.
Why this is a problem
This creates a few rough edges:
Activity inputs in Temporal history are harder to inspect
The recorded activity input contains the unresolved expression rather than the concrete value passed to the external system.
That makes debugging harder because it is not obvious what value the activity actually used.
Expression validation may miss activity input expressions
If expressions inside with payloads are not collected and validated before execution, then invalid, unsupported or non-deterministic expressions may only fail at runtime.
Expression evaluation responsibility is split
Some expression handling happens in the workflow layer, but activity-backed calls may also perform their own interpolation.
This makes behaviour harder to reason about and risks inconsistent handling across activity types such as http, grpc, call and run.
Potential determinism gap
In principle, a non-idempotent expression could be passed into an activity payload and evaluated outside the normal deterministic validation path.
This may be unlikely in common usage, but it weakens the boundary Zigflow is trying to enforce.
Expected behaviour
Before scheduling any activity-backed task, Zigflow should evaluate expressions in the task's with payload using the workflow expression context.
The activity should receive a fully resolved input payload.
For example, if:
arguments:
input: ${ $env.GRPC_INPUT }
and GRPC_INPUT=hello, the activity input should contain:
I've got a working implementation for this, but I'm not entirely convinced about the long-term behaviour around ${ $data.activity.* }.
The difficulty is that activity metadata only exists once an activity has actually been dispatched. Values such as ${ $data.activity.attempt } therefore cannot be resolved by the workflow before scheduling in the same way that ${ $env.* }, ${ $input.* } or ordinary ${ $data.* } expressions can.
The current implementation preserves expressions that reference $data.activity.* and evaluates them inside the activity after activity metadata has been added to the state. This appears to work correctly, but it introduces a split evaluation model and I'm not yet comfortable that we've fully defined the boundary of what should be resolved before scheduling versus what should be preserved for activity-time evaluation.
Leaving this open for now while I think through that contract a bit more.
Hi @mrsimonemms — I'd be happy to help get this one over the line if it's useful to you.
On the $data.activity.* concern from #483: one way to keep it predictable would be to resolve, before scheduling, every activity input that doesn't touch activity state, and leave any input that does exactly as it is today, evaluated inside the activity. Deciding which inputs touch activity state from the parsed expression (gojq's parser) rather than a string prefix would also cover the forms the review flagged (${ $data | .activity.attempt }, ${ $data as $d | $d.activity.attempt }). Erring on the side of "references $data and an activity field anywhere → leave it to the activity" means a false positive only keeps today's behaviour, never resolves something too early.
If that matches what you had in mind, I can rebase #483 onto current main, add tests for those jq forms, and update the docs — or open a fresh PR referencing #462, whichever you prefer. If you'd rather settle the semantics differently (e.g. not supporting $data.activity.* in activity inputs at all), happy to follow that instead.
Yes please. I'll let you decide whether to improve #483 or create a new one (probably the best option). One of the reasons I abandoned this was that there weren't much in the way of unit/e2e tests at the time and that's been improved greatly now.
Thanks @mrsimonemms — I'll open a fresh PR referencing #462, building on the approach from #483: inputs that don't touch activity state are resolved before scheduling, anything referencing $data plus an activity field is left for the activity as today (detected from the parsed expression, so the forms from the #483 review are covered). It'll come with unit tests for the detection and the resolution, and an e2e test. I'll shout if anything needs discussing before then.
Following up after digging into the code — two findings worth aligning on before I open the PR:
Validation is already covered on main.zigflow validate already rejects an invalid expression (${ $env.X | }) and a non-deterministic one (${ uuid }) inside grpc and httpwith payloads, so the PR can focus on resolving inputs before scheduling.
Resolving workflow-side while the activity keeps its own pass evaluates data twice. With Evaluate expressions before executing activities #483's approach, a workflow input comment = "${ $env.API_TOKEN }" used as ${ $input.comment } resolves workflow-side to that literal string, then the activity's TraverseAndEvaluateObj evaluates it again and sends the token's value to the endpoint (I reproduced this against the Evaluate expressions before executing activities #483 branch). Today's single activity-side pass doesn't have this problem.
My proposal: resolve the with payload workflow-side except expressions that reference activity state, and re-emit any resolved value that itself looks like ${ … } as a jq string literal (${ "…" }), so the activity's existing pass returns it unchanged. That keeps the single-evaluation guarantee without changing activity signatures (so mixed-version workers and in-flight runs stay safe), and only that rare case shows the quoted form in history. Scope would be http and grpc; call: activity already resolves its arguments, and run stays activity-side as #483 also left it.
Does that match how you'd like it handled, or would you prefer a different approach to the double evaluation (for example, the activity only evaluating the expressions the workflow preserved)?
My honest take is that I'm not convinced my original pass was correct and I think this should be looked at with fresh eyes (which I'm thrilled that you're doing). I'm happy with the validation (I think there's also an option to validate at runtime) and I'd prefer to only evaluate data once.
Please approach this as a new PR and don't assume that anything I'd already done is anywhere close to correct.
Data is now evaluated once. The workflow resolves each activity's inputs before scheduling it. The activity evaluates only the expressions it is explicitly handed: those that read $data.activity, which cannot exist before an attempt runs, because one scheduled input serves every retry. There's no second pass over resolved data and no quoting.
It covers http, grpc and run (shell, script, container). Open histories stay on today's behaviour through a version marker; I checked that by swapping the worker in the middle of a running execution. The two small behaviour changes are listed in the PR.
If you'd rather not support $data.activity in activity inputs at all, the deferral can go and the activities stop evaluating entirely. Happy to change it either way.
Activity
mrsimonemms commentedon Jun 16, 2026
I've got a working implementation for this, but I'm not entirely convinced about the long-term behaviour around
${ $data.activity.* }.The difficulty is that activity metadata only exists once an activity has actually been dispatched. Values such as
${ $data.activity.attempt }therefore cannot be resolved by the workflow before scheduling in the same way that${ $env.* },${ $input.* }or ordinary${ $data.* }expressions can.The current implementation preserves expressions that reference
$data.activity.*and evaluates them inside the activity after activity metadata has been added to the state. This appears to work correctly, but it introduces a split evaluation model and I'm not yet comfortable that we've fully defined the boundary of what should be resolved before scheduling versus what should be preserved for activity-time evaluation.Leaving this open for now while I think through that contract a bit more.
YJ-928 commentedon Oct 5, 2026
Hi @mrsimonemms — I'd be happy to help get this one over the line if it's useful to you.
On the
$data.activity.*concern from #483: one way to keep it predictable would be to resolve, before scheduling, every activity input that doesn't touch activity state, and leave any input that does exactly as it is today, evaluated inside the activity. Deciding which inputs touch activity state from the parsed expression (gojq's parser) rather than a string prefix would also cover the forms the review flagged (${ $data | .activity.attempt },${ $data as $d | $d.activity.attempt }). Erring on the side of "references$dataand anactivityfield anywhere → leave it to the activity" means a false positive only keeps today's behaviour, never resolves something too early.If that matches what you had in mind, I can rebase #483 onto current
main, add tests for those jq forms, and update the docs — or open a fresh PR referencing #462, whichever you prefer. If you'd rather settle the semantics differently (e.g. not supporting$data.activity.*in activity inputs at all), happy to follow that instead.mrsimonemms commentedon Oct 5, 2026
Yes please. I'll let you decide whether to improve #483 or create a new one (probably the best option). One of the reasons I abandoned this was that there weren't much in the way of unit/e2e tests at the time and that's been improved greatly now.
Please shout if you want help/discuss thing
YJ-928 commentedon Oct 6, 2026
Thanks @mrsimonemms — I'll open a fresh PR referencing #462, building on the approach from #483: inputs that don't touch activity state are resolved before scheduling, anything referencing
$dataplus anactivityfield is left for the activity as today (detected from the parsed expression, so the forms from the #483 review are covered). It'll come with unit tests for the detection and the resolution, and an e2e test. I'll shout if anything needs discussing before then.YJ-928 commentedon Oct 6, 2026
Following up after digging into the code — two findings worth aligning on before I open the PR:
Validation is already covered on
main.zigflow validatealready rejects an invalid expression (${ $env.X | }) and a non-deterministic one (${ uuid }) insidegrpcandhttpwithpayloads, so the PR can focus on resolving inputs before scheduling.Resolving workflow-side while the activity keeps its own pass evaluates data twice. With Evaluate expressions before executing activities #483's approach, a workflow input
comment = "${ $env.API_TOKEN }"used as${ $input.comment }resolves workflow-side to that literal string, then the activity'sTraverseAndEvaluateObjevaluates it again and sends the token's value to the endpoint (I reproduced this against the Evaluate expressions before executing activities #483 branch). Today's single activity-side pass doesn't have this problem.My proposal: resolve the
withpayload workflow-side except expressions that reference activity state, and re-emit any resolved value that itself looks like${ … }as a jq string literal (${ "…" }), so the activity's existing pass returns it unchanged. That keeps the single-evaluation guarantee without changing activity signatures (so mixed-version workers and in-flight runs stay safe), and only that rare case shows the quoted form in history. Scope would behttpandgrpc;call: activityalready resolves its arguments, andrunstays activity-side as #483 also left it.Does that match how you'd like it handled, or would you prefer a different approach to the double evaluation (for example, the activity only evaluating the expressions the workflow preserved)?
mrsimonemms commentedon Oct 7, 2026
My honest take is that I'm not convinced my original pass was correct and I think this should be looked at with fresh eyes (which I'm thrilled that you're doing). I'm happy with the validation (I think there's also an option to validate at runtime) and I'd prefer to only evaluate data once.
Please approach this as a new PR and don't assume that anything I'd already done is anywhere close to correct.
YJ-928 commentedon Oct 8, 2026
Thanks, I took it from scratch: #615.
Data is now evaluated once. The workflow resolves each activity's inputs before scheduling it. The activity evaluates only the expressions it is explicitly handed: those that read
$data.activity, which cannot exist before an attempt runs, because one scheduled input serves every retry. There's no second pass over resolved data and no quoting.It covers
http,grpcandrun(shell, script, container). Open histories stay on today's behaviour through a version marker; I checked that by swapping the worker in the middle of a running execution. The two small behaviour changes are listed in the PR.If you'd rather not support
$data.activityin activity inputs at all, the deferral can go and the activities stop evaluating entirely. Happy to change it either way.fix: resolve activity inputs in the workflow before scheduling
fix: resolve activity inputs in the workflow before scheduling
fix: resolve activity inputs in the workflow before scheduling