cloudflare / cloudflare/dynamic-workflows

The message-based NonRetryableError escape hatch also fails to settle the instance (sibling of #6)

Open
#7 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
43
Forks
8
PR merge metrics
No merged PRs in 30d

Description

### Summary

Sibling of #6. That issue reports `NonRetryableError` losing its class across the runner stub, so the engine retries. This one reports that the engine's **message-based** escape hatch — which does survive the stub — does not make the instance settle either.

The engine's own check is name-or-message, not `instanceof` (miniflare `dist/src/workers/workflows/binding.worker.js`):

```js
err.name === 'NonRetryableError' || err.message.startsWith('NonRetryableError')
```

Since #6 establishes that the name is dropped crossing the stub while the message survives, prefixing the message looks like a valid workaround from tenant code that cannot import `cloudflare:workflows`. It isn't.

Version: `@cloudflare/dynamic-workflows@0.1.1`, `wrangler@4.120.1`, local `wrangler dev`.

### Repro

Same dispatcher wiring as #6 — `createDynamicWorkflowEntrypoint`, `DynamicWorkflowBinding` re-exported, instances via `wrapWorkflowBinding(metadata)`, tenant module through `env.LOADER.get(id, …)` with `globalOutbound: null`. The tenant throws **inside** `step.do`, with both signals set and without importing `cloudflare:workflows`:

```js
export default {
async run(gateway, event, step) {
return step.do('denied_step', async () => {
const fatal = new Error('NonRetryableError: policy_violation')
fatal.name = 'NonRetryableError'
throw fatal
})
}
}
```

### Expected

The step fails immediately; the instance reaches `errored` once.

### Actual

The instance does not reach a terminal status. Polling `instance.status()` every 250ms for 10s never observes `errored`, `complete` or `terminated`, and the sentinel message appears several times in that window, consistent with the step still being retried.

One observation I could not fully resolve: the message comes back with the `NonRetryableError: ` prefix **stripped**, which in the miniflare source only happens when `PreservedNonRetryableError` is constructed. That means a fatal branch is reached — but I could not determine whether it is the step-level branch or the outer one after the retry budget is exhausted, so I am not claiming the step-level check matched.

### Why it matters

#6's workaround-shaped reading is "set the message prefix instead of relying on the class". If that also fails, tenant code inside a Dynamic Worker has no way at all to mark a failure terminal, and every deterministic failure — a validation error from a downstream API, for instance — costs the full retry budget. In our case a rejected write took six attempts across roughly thirty seconds to fail in a way that was decided in sixty milliseconds.

Happy to test a patch or provide a fuller harness.

Contributor guide

Open the contributing guide

Research direction

Start with the retry and settlement logic in miniflare dist/src/workers/workflows/binding.worker.js, then reproduce the tenant step.do case using the dispatcher wiring described in the issue. Trace whether the message-based NonRetryableError check reaches the step-level or outer branch, and verify that the instance reaches errored once without repeated attempts.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
backend, distributed-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.