Skip to content

feat: add Workflows support#918

Open
connyay wants to merge 22 commits intocloudflare:mainfrom
connyay:cjh-workflow
Open

feat: add Workflows support#918
connyay wants to merge 22 commits intocloudflare:mainfrom
connyay:cjh-workflow

Conversation

@connyay
Copy link
Copy Markdown
Contributor

@connyay connyay commented Jan 28, 2026

Add bindings for Workflows including step primitives (do, sleep, sleepUntil), event/instance management, and a proc macro for defining workflow entrypoints. Includes example worker, sys-level bindings, and integration tests.

Add bindings for Workflows including step primitives
(do, sleep, sleepUntil), event/instance management, and a
proc macro for defining workflow entrypoints. Includes example
worker, sys-level bindings, and integration tests.
@connyay
Copy link
Copy Markdown
Contributor Author

connyay commented Jan 28, 2026

Fixes #663 👏

@connyay connyay mentioned this pull request Jan 28, 2026
1 task
@guybedford
Copy link
Copy Markdown
Collaborator

Amazing, I will take a look soon. Hopefully we can get this in for the next release!

connyay and others added 5 commits January 28, 2026 08:18
clippy fix picked up more things. unclear why the build just flagged
these
Workflow async methods were failing to compile with the `http` feature
because JsFuture is not Send. Wrap all JsFuture calls with SendFuture
and extract promises to separate variables to avoid holding non-Send
types across await points.
Copy link
Copy Markdown
Collaborator

@guybedford guybedford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some initial comments. Let me know your thoughts.

Comment thread worker/src/workflow.rs
Comment thread worker-sys/src/types/workflow.rs Outdated
- Bind NonRetryableError to the actual JS class from cloudflare:workflows
  so the runtime correctly identifies non-retryable errors
- Add --external cloudflare:workflows to esbuild in worker-build
- Change wasm_bindgen extern bindings to async with return typing
- Change step callbacks from FnOnce to Fn so retries can re-invoke them
- Preserve Error::Internal JsValues through error conversion chain
- Update workflow example to demonstrate both retryable and non-retryable
  error patterns with request body parsing
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	worker-build/src/main.rs
@connyay connyay requested a review from guybedford February 26, 2026 01:14
…ers, improve efficiency

- Exclude __wf_* marker functions from generate_handlers to prevent spurious Entrypoint.prototype entries
- Replace raw Reflect::set calls in send_event with a single serde serialization
- Cache result_array.length() in create_batch to avoid redundant JS boundary crossings
- Reorder expand_macro to parse ItemStruct first, avoiding unnecessary clone+parse on success path
- Extract shared create_workflow_with_value helper from duplicated test handlers
- Poll immediately before sleeping in test loops to reduce CI latency
Exercises the WorkflowStepEvent::from_js deserialization and
WorkflowInstance::send_event serialization round-trip that wasn't
covered by existing tests.
Comment thread test/src/durable.rs Outdated
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 1, 2026

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 2 untouched benchmarks


Comparing connyay:cjh-workflow (187dad2) with main (38d7b68)

Open in CodSpeed

- Use next_back() instead of last() on DoubleEndedIterator (clippy lint)
- Use Closure::new instead of Closure::wrap to preserve UnwindSafe through the concrete closure type rather than erasing it via trait object cast
Copy link
Copy Markdown
Collaborator

@guybedford guybedford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very close to a perfect implementation, some small suggestions. Probably worth carefully working through this before landing so I won't rush it out in todays release actually.

Comment thread examples/workflow/src/lib.rs Outdated

async fn run(
&self,
event: WorkflowEvent<serde_json::Value>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we come up with a better typing solution here? It is quite restrictive to rely so heavily on serde. Can we instead make this generic with the bound being something something as simple as IntoWasmAbi?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does JsValue work here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean specifically is making this a generic argument T so that run<T> can take any type with the IntoWasmAbi trait from wasm bindgen?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually with the Wasm Bindgen generics you could just make T: JsGeneric on the new JsGeneric trait.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsGeneric is a bit awkward to work with here.

/// The event passed to a workflow's run method.
#[derive(Debug, Clone)]
pub struct WorkflowEvent<T = JsValue> {
    pub payload: T,
    pub timestamp: crate::Date,
    pub instance_id: String,
}

impl<T: JsGeneric> WorkflowEvent<T> {
    pub fn from_js(value: JsValue) -> Result<Self> {
        let payload = get_property(&value, "payload")?;
        Ok(Self {
            // SAFETY: JsGeneric guarantees T has ErasableGeneric<Repr = JsValue>,
            // so T and JsValue have an identical memory layout.
            payload: unsafe {
                core::mem::transmute_copy(&core::mem::ManuallyDrop::new(payload))
            },
            timestamp: get_timestamp_property(&value, "timestamp")?,
            instance_id: get_string_property(&value, "instanceId")?,
        })
    }
}

JsCast is slightly less awkward:

/// The event passed to a workflow's run method.
#[derive(Debug, Clone)]
pub struct WorkflowEvent<T = JsValue> {
    pub payload: T,
    pub timestamp: crate::Date,
    pub instance_id: String,
}

impl<T: JsCast> WorkflowEvent<T> {
    pub fn from_js(value: JsValue) -> Result<Self> {
        Ok(Self {
            payload: get_property(&value, "payload")?
                .dyn_into()
                .map_err(|_| crate::Error::JsError("payload is not the expected type".into()))?,
            timestamp: get_timestamp_property(&value, "timestamp")?,
            instance_id: get_string_property(&value, "instanceId")?,
        })
    }
}

thoughts/opinions? or am I missing something/doing something wrong with JsGeneric?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I likely need to properly follow the API design details here, I will aim to do a thorough review next week after this current release. Sorry for the delay and thanks for the patience as always!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good! Thank you!

Comment thread worker/src/workflow.rs Outdated
Comment thread worker/src/workflow.rs Outdated
Comment thread worker/src/workflow.rs Outdated
Comment thread worker/src/workflow.rs Outdated
connyay added 3 commits April 1, 2026 20:32
* remove fetcher
* remove result from id()
* event_type -> type_
* workflowsleepduration
serde_json::Value -> JsValue
noticed while looking at the workerd types
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.

2 participants