Skip to content
142 changes: 142 additions & 0 deletions src/content/docs/workflows/build/call-workflows-from-pages.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
title: Call Workflows from Pages
pcx_content_type: concept
sidebar:
order: 11
---

import { WranglerConfig, TypeScriptExample } from "~/components";

You can bind and trigger Workflows from [Pages Functions](/pages/functions/) by deploying a Workers project with your Workflow definition and then invoking that Worker using [service bindings](/pages/functions/bindings/#service-bindings) or a standard `fetch()` call.

:::note

You will need to deploy your Workflow as a standalone Workers project first before your Pages Function can call it. Visit the Workflows [get started guide](/workflows/get-started/guide/) if you have not yet deployed a Workflow.

:::

### Use Service Bindings

[Service Bindings](/workers/runtime-apis/bindings/service-bindings/) allow you to call a Worker from another Worker or a Pages Function without needing to expose it directly.

To do this, you will need to:

1. Deploy your Workflow in a Worker
2. Create a Service Binding to that Worker in your Pages project
3. Call the Worker remotely using the binding

For example, if you have a Worker called `workflows-starter`, you would create a new Service Binding in your Pages project as follows, ensuring that the `service` name matches the name of the Worker your Workflow is defined in:

<WranglerConfig>
```toml
services = [
{ binding = "WORKFLOW_SERVICE", service = "workflows-starter" }
]
```
</WranglerConfig>

Your Worker can expose a specific method (or methods) that only other Workers or Pages Functions can call over the Service Binding.

In the following example, we expose a specific `createInstance` method that accepts our `Payload` and returns the [`InstanceStatus`](/workflows/build/workers-api/#instancestatus) from the Workflows API:

<TypeScriptExample filename="index.ts">
```ts
import { WorkerEntrypoint } from "cloudflare:workers";

interface Env {
MY_WORKFLOW: Workflow;
}

type Payload = {
hello: string;
}

export default class WorkflowsService extends WorkerEntrypoint<Env> {
// Currently, entrypoints without a named handler are not supported
async fetch() { return new Response(null, {status: 404}); }

async createInstance(payload: Payload) {
let instance = await this.env.MY_WORKFLOW.create({
params: payload
});

return Response.json({
id: instance.id,
details: await instance.status(),
});
}
}
```
</TypeScriptExample>

Your Pages Function would resemble the following:

<TypeScriptExample filename="functions/request.ts">
```ts
interface Env {
WORKFLOW_SERVICE: Service;
}

export const onRequest: PagesFunction<Env> = async (context) => {
// This payload could be anything from within your app or from your frontend
let payload = {"hello": "world"}
return context.env.WORKFLOWS_SERVICE.createInstance(payload)
};
```
</TypeScriptExample>

Visit the [bindings documentation for Pages Functions](/pages/functions/bindings/#service-bindings) to learn more about binding to resources from Pages Functions, including how to bind via the Cloudflare dashboard.

### Using fetch

:::note "Service Bindings vs. fetch"

We recommend using [Service Bindings](/workers/runtime-apis/bindings/service-bindings/) when calling a Worker in your own account.

Service Bindings don't require you to expose a public endpoint from your Worker, don't require you to configure authentication, and allow you to call methods on your Worker directly, avoiding the overhead of managing HTTP requests and responses.

:::

An alternative to setting up a Service Binding is to call the Worker over HTTP by using the Workflows [Workers API](/workflows/build/workers-api/#workflow) to `create` a new Workflow instance for each incoming HTTP call to the Worker:

<TypeScriptExample filename="index.ts">
```ts
// This is in the same file as your Workflow definition
export default {
async fetch(req: Request, env: Env): Promise<Response> {
let instance = await env.MY_WORKFLOW.create({
params: payload
});
return Response.json({
id: instance.id,
details: await instance.status(),
});
},
};
```
</TypeScriptExample>

Your [Pages Function](/pages/functions/get-started/) can then make a regular `fetch` call to the Worker:

<TypeScriptExample filename="functions/request.ts">
```ts
export const onRequest: PagesFunction<Env> = async (context) => {
// Other code
let payload = {"hello": "world"}
const instanceStatus = await fetch("https://YOUR_WORKER.workers.dev/", {
method: "POST",
body: JSON.stringify(payload) // Send a payload for our Worker to pass to the Workflow
})

return Response.json(instanceStatus);
};
```
</TypeScriptExample>

You can also choose to authenticate these requests by passing a shared secret in a header and validating that in your Worker.

### Next steps

* Learn more about how to programatically call and trigger Workflows from the [Workers API](/workflows/build/workers-api/)
* Understand how to send [events and parameters](/workflows/build/events-and-parameters/) when triggering a Workflow
* Review the [Rules of Workflows](/workflows/build/rules-of-workflows/) and best practices for writing Workflows
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ sidebar:

---

import { MetaInfo, Render, Type } from "~/components";
import { MetaInfo, Render, Type, WranglerConfig, TypeScriptExample } from "~/components";

When a Workflow is triggered, it can receive an optional event. This event can include data that your Workflow can act on, including request details, user data fetched from your database (such as D1 or KV) or from a webhook, or messages from a Queue consumer.

Expand Down
57 changes: 56 additions & 1 deletion src/content/docs/workflows/build/rules-of-workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ sidebar:
order: 10
---

import { WranglerConfig, TypeScriptExample } from "~/components";

A Workflow contains one or more steps. Each step is a self-contained, individually retriable component of a Workflow. Steps may emit (optional) state that allows a Workflow to persist and continue from that step, even if a Workflow fails due to a network or infrastructure issue.

This is a small guidebook on how to build more resilient and correct Workflows.
Expand All @@ -17,6 +19,7 @@ can be applied multiple times without changing the result beyond the initial app
As an example, let us assume you have a Workflow that charges your customers, and you really do not want to charge them twice by accident. Before charging them, you should
check if they were already charged:

<TypeScriptExample filename="index.ts">
```ts
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
Expand Down Expand Up @@ -52,6 +55,7 @@ export class MyWorkflow extends WorkflowEntrypoint {
}
}
```
</TypeScriptExample>

:::note

Expand All @@ -67,6 +71,7 @@ You can also think of it as a transaction, or a unit of work.

- ✅ Minimize the number of API/binding calls per step (unless you need multiple calls to prove idempotency).

<TypeScriptExample filename="index.ts">
```ts
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
Expand All @@ -84,6 +89,7 @@ export class MyWorkflow extends WorkflowEntrypoint {
}
}
```
</TypeScriptExample>

Otherwise, your entire Workflow might not be as durable as you might think, and you may encounter some undefined behaviour. You can avoid them by following the rules below:

Expand All @@ -92,6 +98,7 @@ Otherwise, your entire Workflow might not be as durable as you might think, and
- 🔴 Do not make too many service calls in the same step (unless you need it to prove idempotency).
- 🔴 Do not do too much CPU-intensive work inside a single step - sometimes the engine may have to restart, and it will start over from the beginning of that step.

<TypeScriptExample filename="index.ts">
```ts
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
Expand All @@ -105,13 +112,15 @@ export class MyWorkflow extends WorkflowEntrypoint {
}
}
```
</TypeScriptExample>

### Do not rely on state outside of a step

Workflows may hibernate and lose all in-memory state. This will happen when engine detects that there is no pending work and can hibernate until it needs to wake-up (because of a sleep, retry, or event).

This means that you should not store state outside of a step:

<TypeScriptExample filename="index.ts">
```ts
function getRandomInt(min, max) {
const minCeiled = Math.ceil(min);
Expand Down Expand Up @@ -152,9 +161,11 @@ export class MyWorkflow extends WorkflowEntrypoint {
}
}
```
</TypeScriptExample>

Instead, you should build top-level state exclusively comprised of `step.do` returns:

<TypeScriptExample filename="index.ts">
```ts
function getRandomInt(min, max) {
const minCeiled = Math.ceil(min);
Expand Down Expand Up @@ -192,11 +203,13 @@ export class MyWorkflow extends WorkflowEntrypoint {
}
}
```
</TypeScriptExample>

### Do not mutate your incoming events

The `event` passed to your Workflow's `run` method is immutable: changes you make to the event are not persisted across steps and/or Workflow restarts.

<TypeScriptExample filename="index.ts">
```ts
interface MyEvent {
user: string;
Expand Down Expand Up @@ -224,12 +237,13 @@ export class MyWorkflow extends WorkflowEntrypoint {
// Will always be the same if this step is retried
})
```
</TypeScriptExample>

### Name steps deterministically

Steps should be named deterministically (even if dynamic). This ensures that their state is cached, and prevents the step from being rerun unnecessarily. Step names act as the "cache key" in your Workflow.


<TypeScriptExample filename="index.ts">
```ts
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
Expand Down Expand Up @@ -261,3 +275,44 @@ export class MyWorkflow extends WorkflowEntrypoint {
})
}
```
</TypeScriptExample>

### Instance IDs are unique

Workflow [instance IDs](/workflows/build/workers-api/#workflowinstance) are unique per Workflow. The ID is the the unique identifier that associates logs, metrics, state and status of a run to a specific an instance, even after completion. Allowing ID re-use would make it hard to understand if a Workflow instance ID referred to an instance that run yesterday, last week or today.

It would also present a problem if you wanted to run multiple different Workflow instances with different [input parameters](/workflows/build/events-and-parameters/) for the same user ID, as you would immediately need to determine a new ID mapping.

If you need to associate multiple instances with a specific user, merchant or other "customer" ID in your system, consider using a composite ID or using randomly generated IDs and storing the mapping in a database like [D1](/d1/).

<TypeScriptExample filename="index.ts">
```ts
// This is in the same file as your Workflow definition
export default {
async fetch(req: Request, env: Env): Promise<Response> {
// 🔴 Bad: Use an ID that isn't unique across future Workflow invocations
let userId = getUserId(req) // Returns the userId
let instance = await env.MY_WORKFLOW.create({
id: userId,
params: payload
});

// ✅ Good: use a composite ID or an
let instanceId = getTransactionId() // e.g. assuming transaction IDs are unique
// or: compose a composite ID and store it in your database
// so that you can track all instances associated with a specific user or merchant.
let instanceId = `${getUserId(request}-${await crypto.randomUUID().slice(0, 6)}`
let { result } = await addNewInstance(userId, instanceId)
let instance = await env.MY_WORKFLOW.create({
id: userId,
params: payload
});

return Response.json({
id: instance.id,
details: await instance.status(),
});
},
};
```
</TypeScriptExample>
11 changes: 7 additions & 4 deletions src/content/docs/workflows/build/workers-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,20 @@ binding = "MY_WORKFLOW"
# this is class that extends the Workflow class in src/index.ts
class_name = "MyWorkflow"
```

</WranglerConfig>

### Bind from Pages

You can bind and trigger Workflows from [Pages Functions](/pages/functions/) by deploying a Workers project with your Workflow definition and then invoking that Worker using [service bindings](/pages/functions/bindings/#service-bindings) or a standard `fetch()` call.

Visit the documentation on [calling Workflows from Pages](/workflows/build/call-workflows-from-pages/) for examples.

### Cross-script calls

You can also bind to a Workflow that is defined in a different Worker script from the script your Workflow definition is in. To do this, provide the `script_name` key with the name of the script to the `[[workflows]]` binding definition in your `wrangler.toml` configuration.

For example, if your Workflow is defined in a Worker script named `billing-worker`, but you are calling it from your `web-api-worker` script, your `wrangler.toml` would resemble the following:



<WranglerConfig>

```toml title="wrangler.toml"
Expand Down Expand Up @@ -228,7 +231,7 @@ export default {
// params expects the type User
params: user
})

return Response.json({
id: instance.id,
details: await instance.status(),
Expand Down
Loading