Skip to content

fix: auto-report unhandled errors in AgentWorkflow to trigger onWorkflowError - #1003

Merged
threepointone merged 1 commit into
mainfrom
fix-workflows-error-reporting
Feb 26, 2026
Merged

fix: auto-report unhandled errors in AgentWorkflow to trigger onWorkflowError#1003
threepointone merged 1 commit into
mainfrom
fix-workflows-error-reporting

Conversation

@threepointone

Copy link
Copy Markdown
Contributor

Closes #998

Problem

AgentWorkflow has an inconsistency in error handling:

  • throw new Error() halts the workflow but never triggers onWorkflowError on the Agent
  • step.reportError() triggers onWorkflowError but does not halt the workflow

Users expect throw to both halt the workflow and notify the Agent. Today they have to do both manually, and even then the ordering is fragile.

Solution

Wrap both originalRun.call() paths in the run() wrapper with try/catch blocks. When an unhandled error propagates out of the user's run() method:

  1. _autoReportError(err) sends an error callback to the Agent via RPC
  2. The original error is re-thrown so the Workflows runtime still sees it as a failure

A new _errorReported instance flag prevents double notification when step.reportError() was already called before the throw.

Changes

File What changed
packages/agents/src/workflows.ts Added _errorReported flag, _autoReportError() method, and try/catch wrappers around both originalRun.call() paths
packages/agents/src/tests/test-workflow.ts 4 new test workflow classes for error scenarios
packages/agents/src/tests/workflow-error-reporting.test.ts New file — 7 integration tests using introspectWorkflowInstance
packages/agents/src/tests/workflow-prototype.test.ts 1 new prototype structural test
packages/agents/src/tests/agents/workflow.ts New WorkflowEnv entries + 4 helper methods for starting error-scenario workflows
packages/agents/src/tests/worker.ts Exports + Env type for new workflows
packages/agents/src/tests/wrangler.jsonc 4 new workflow bindings
.changeset/fix-workflow-error-reporting.md Changeset (patch)

Design decisions & tradeoffs

Non-durable error notification

_autoReportError calls notifyAgent() directly — it is not wrapped in step.do(). This is intentional:

  • The workflow is about to halt anyway; durability of the notification is less important than ensuring the original error propagates cleanly
  • Wrapping in step.do() could interfere with the Workflows runtime's retry semantics (the step might succeed on retry while the throw still happens)
  • On workflow retry, a new instance is created with _errorReported = false, so the agent gets re-notified — this is acceptable because onWorkflowCallback idempotently sets status to "errored"

Best-effort notification

The try/catch inside _autoReportError swallows notification failures. If the agent is unreachable (e.g., binding misconfigured, DO overloaded), the original error still propagates. The workflow enters "errored" state in the Workflows runtime regardless.

Flag set before step.do

In reportError(), this._errorReported = true is set before the step.do() call. This ensures the guard works even if:

  • The durable step fails or is mocked in tests
  • The workflow is interrupted between the flag set and step completion

_initAgent failure path

If _initAgent() throws (e.g., missing binding), the catch calls _autoReportError, which calls this.agent getter, which throws "Agent not initialized". The inner try/catch swallows this — correct behavior since we can't notify an agent we can't reach.

Inheritance / super.run() path

Both branches of the run() wrapper have try/catch. If a child class calls super.run() and the parent throws, both wrappers catch. The _errorReported flag (instance-level) prevents the second wrapper from sending a duplicate notification.

Edge cases covered by tests

Scenario Test Result
throw directly in run() (outside step.do) ✅ passes Agent receives exactly 1 error callback
throw inside step.do() ⏭️ skipped Workflows runtime retries steps with backoff, causing test timeout. Covered by the run() test since step errors propagate up.
step.reportError() then throw ✅ passes Only 1 error callback (flag prevents duplicate)
step.reportError() alone (no throw) ✅ passes Workflow continues and completes (backward compat)
Throw non-Error value (string) ✅ passes String(err) path works correctly
Agent unreachable during notification ✅ passes Original error still propagates, workflow errors
waitForApproval rejection ✅ passes reportError inside waitForApproval sets flag, auto-report skipped

Notes for reviewers

  • The step.do() throw test is skipped because the Workflows runtime retries failed steps with exponential backoff, causing waitForStatus("errored") to exceed the 5s test timeout. The behavior is still correct — step errors propagate to run() where our catch fires. This is the same limitation that causes 5 other tests in workflow-integration.test.ts to be skipped.
  • _autoReportError is a private method, so no public API surface change. The only observable behavior change is that onWorkflowError now fires on unhandled throws.
  • The _errorReported flag resets on each workflow instance. If the Workflows runtime retries run() on a new instance, the agent gets notified again. This is by design — each retry is a fresh attempt, and repeated "errored" status updates are idempotent.
  • The changeset is a patch bump since this is a bug fix with no API changes.

…lowError

Closes #998

Previously, throwing an error inside a workflow's run() method would halt
the workflow but never notify the Agent via onWorkflowError. Only explicit
step.reportError() calls triggered the callback, but those did not halt
the workflow. This created an inconsistency where users had to choose
between halting (throw) and notifying (reportError).

The fix wraps both originalRun.call() paths in the run() wrapper with
try/catch blocks that call a new _autoReportError() method. This method
sends an error callback to the Agent via RPC, then re-throws the original
error so the workflow still halts.

A _errorReported flag prevents double notification when step.reportError()
is called before throwing (e.g., in waitForApproval rejection flows).

Key design decisions:
- _autoReportError is non-durable (not wrapped in step.do) because the
  workflow is about to halt anyway. On retry, a new instance resets the
  flag, so the agent gets re-notified (idempotent via onWorkflowCallback).
- Best-effort: notification failures are swallowed so the original error
  always propagates to the Workflows runtime.
- The flag is set before step.do in reportError, so it guards even when
  steps are mocked or fail during testing.
@changeset-bot

changeset-bot Bot commented Feb 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9f576fb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Feb 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@1003
npm i https://pkg.pr.new/cloudflare/agents/@cloudflare/ai-chat@1003
npm i https://pkg.pr.new/cloudflare/agents/@cloudflare/codemode@1003
npm i https://pkg.pr.new/cloudflare/agents/hono-agents@1003

commit: 9f576fb

@threepointone
threepointone merged commit d24936c into main Feb 26, 2026
4 checks passed
@threepointone
threepointone deleted the fix-workflows-error-reporting branch February 26, 2026 17:19
@github-actions github-actions Bot mentioned this pull request Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

onWorkflowError is only triggered by step.reportError(), but ignores standard throw inside workflows

1 participant