cloudflare / cloudflare/dynamic-workflows
The message-based NonRetryableError escape hatch also fails to settle the instance (sibling of #6)
- 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
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