Repository navigation
fix(triggers): make expression evaluation errors non-retryable - #215
Merged
Merged
Conversation
expr compile and runtime errors are deterministic for a given payload and expression, but were returned as plain errors from EvalTriggerVariables, so Temporal retried them forever and ExecuteTrigger never recorded the failed occurrence nor published FAILED_TRIGGER. Wrap them in a non-retryable ApplicationError of type EXPRESSION_EVALUATION. Errors returned by the link() function are tagged so they are passed through unchanged: transient HTTP/decoding failures stay retryable and link()'s own non-retryable APPLICATION errors are preserved. Filter evaluation still maps errors to a non-match.
link() now returns retryable ApplicationErrors for transient HTTP failures and non-retryable ones for deterministic argument errors. EvalTriggerVariables wraps any other evaluation error as non-retryable EXPRESSION_EVALUATION, so the shared evaluator stays Temporal-agnostic and the TestTrigger API keeps returning plain expr messages.
NumaryBot
previously approved these changes
Sep 29, 2026
NumaryBot
left a comment
Contributor
There was a problem hiding this comment.
The required automated review completed with no remaining findings.
flemzord
reviewed
Sep 29, 2026
NumaryBot
previously approved these changes
Oct 7, 2026
NumaryBot
left a comment
Contributor
There was a problem hiding this comment.
The required automated review completed with no remaining findings.
NumaryBot
approved these changes
Oct 7, 2026
NumaryBot
left a comment
Contributor
There was a problem hiding this comment.
The required automated review completed with no remaining findings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A trigger whose variable expression cannot be evaluated against the event wedges
ExecuteTriggerforever.EvalTriggerVariablesreturned expr compile/runtime errors as plain Go errors, and the activity runs with Temporal's default (unlimited) retry policy. Errors against the event payload alone are deterministic — the same payload fails the same expression on every attempt — so theif err != nil { occurrence.Error = ... }branch inExecuteTrigger, which records the failed occurrence and publishesFAILED_TRIGGER, was never reached.Seen in production: a trigger on the Connectivity event
SAVED_PAYMENT_INITIATION_ADJUSTMENT(payload has notransactions) with the variablefails with
cannot fetch 0 from <nil> (1:23)on every attempt, thousands of times.Fix
EvalTriggerVariablesclassifies compile errors and runtime errors before any successfullink()fetch as non-retryableEXPRESSION_EVALUATIONerrors. Runtime errors after a successful fetch stay retryable because the remote data can change. The fetch marker is local to each expression evaluation, including concurrent calls; explicitly classifiedlink()errors retain their original retryability. The classification lives at the activity boundary so the shared evaluator stays Temporal-agnostic: theTestTriggerAPI keeps returning the plain expr message.link()now classifies its own errors, since expr wraps custom-function errors the same way as its own engine errors (e.g. an invalid regexp), so they can't be told apart after the fact:ApplicationError(typeLINK);APPLICATION, like the existing unknown/multiple-link cases.link()fetch are conservatively retryable even when a later error in that same expression is unrelated to the fetched data. They remain bounded by the activity policy.Tests
internal/triggers/expression_test.go— table test throughEvalTriggerVariables: the production payload/expression, a compile error, an invalid regexp andlink()argument errors are all non-retryable; permanentlink()4xx responses stop retries; HTTP 408/429/500, bad body and connection refused stay retryable; a failing filter still doesn't match.internal/triggers/workflow_trigger_expression_test.go—ExecuteTriggerwith the production case or an HTTP 404 linked resource runsEvalTriggerVariablesexactly once, records the occurrence with the expr error and no workflow instance, and sends the termination event. Additional workflow tests verify that incomplete HTTP 200 linked data succeeds on the second attempt and starts the child workflow once, while persistently incomplete data records a failed occurrence after 15 attempts. Classification tests also cover compile errors mentioninglink(), unexecuted links, permanent link errors following a successful fetch, and isolation between concurrent expressions.go build ./...,go vet -tags it ./internal/...andgo test -race -tags it ./...all pass.Deploying this
This is an activity-code change, so it does reach workflows already wedged on this error: their next retry runs on the new worker and fails non-retryably (within ~100s, the default max backoff). Each one then records a failed occurrence and publishes
FAILED_TRIGGER— expect a burst of those events for affected stacks on rollout.Minor behaviour changes
TestTriggerAPI: a transientlink()failure message now carries the Temporal suffix(type: LINK, retryable: true), like the non-retryablelink()errors already did.(type: EXPRESSION_EVALUATION, retryable: false).PR #216 (bounded retry policies for all activities) is merged into
mainand included in this branch. Already scheduled retries can still retain their old policy; permanent expression and linked-resource errors stop immediately with the new activity code.