Workflow 包含一个或多个步骤。每个步骤是 Workflow 中自包含、可单独重试的组件。步骤可以发出(可选的)状态,使 Workflow 能够持久化并从该步骤继续执行,即使 Workflow 因网络或基础设施问题而失败。
这是一本关于如何构建更具 resilient 且正确的 Workflows 的小型指南。
由于步骤可能被重试多次,你的步骤(理想情况下)应具有幂等性。作为背景,幂等性是一种逻辑属性,即操作(在此情况下为步骤)可以多次应用,而不会改变初始应用之外的结果。
例如,假设你有一个向客户收费的 Workflow,你绝对不希望意外重复收费。在收费之前,你应该检查他们是否已被收费:
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const customer_id = 123456;
// ✅ Good: Non-idempotent API/Binding calls are always done **after** checking if the operation is
// still needed.
await step.do(
`charge ${customer_id} for its monthly subscription`,
async () => {
// API call to check if customer was already charged
const subscription = await fetch(
`https://payment.processor/subscriptions/${customer_id}`,
).then((res) => res.json());
// return early if the customer was already charged, this can happen if the destination service dies
// in the middle of the request but still commits it, or if the Workflows Engine restarts.
if (subscription.charged) {
return;
}
// non-idempotent call, this operation can fail and retry but still commit in the payment
// processor - which means that, on retry, it would mischarge the customer again if the above checks
// were not in place.
return await fetch(
`https://payment.processor/subscriptions/${customer_id}`,
{
method: "POST",
body: JSON.stringify({ amount: 10.0 }),
},
);
},
);
}
}export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const customer_id = 123456;
// ✅ Good: Non-idempotent API/Binding calls are always done **after** checking if the operation is
// still needed.
await step.do(
`charge ${customer_id} for its monthly subscription`,
async () => {
// API call to check if customer was already charged
const subscription = await fetch(
`https://payment.processor/subscriptions/${customer_id}`,
).then((res) => res.json());
// return early if the customer was already charged, this can happen if the destination service dies
// in the middle of the request but still commits it, or if the Workflows Engine restarts.
if (subscription.charged) {
return;
}
// non-idempotent call, this operation can fail and retry but still commit in the payment
// processor - which means that, on retry, it would mischarge the customer again if the above checks
// were not in place.
return await fetch(
`https://payment.processor/subscriptions/${customer_id}`,
{
method: "POST",
body: JSON.stringify({ amount: 10.0 }),
},
);
},
);
}
}步骤应尽可能自包含。这使你的逻辑在第三方 API 失败、网络错误等情况下更具 durable 性。
你也可以将其视为事务或工作单元。
- ✅ 尽量减少每个步骤中的 API/绑定调用次数(除非你需要多次调用来证明幂等性)。
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// ✅ Good: Unrelated API/Binding calls are self-contained, so that in case one of them fails
// it can retry them individually. It also has an extra advantage: you can control retry or
// timeout policies for each granular step - you might not to want to overload http.cat in
// case of it being down.
const httpCat = await step.do("get cutest cat from KV", async () => {
return await this.env.KV.get("cutest-http-cat");
});
const image = await step.do("fetch cat image from http.cat", async () => {
return await fetch(`https://http.cat/${httpCat}`);
});
}
}export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// ✅ Good: Unrelated API/Binding calls are self-contained, so that in case one of them fails
// it can retry them individually. It also has an extra advantage: you can control retry or
// timeout policies for each granular step - you might not to want to overload http.cat in
// case of it being down.
const httpCat = await step.do("get cutest cat from KV", async () => {
return await this.env.KV.get("cutest-http-cat");
});
const image = await step.do("fetch cat image from http.cat", async () => {
return await fetch(`https://http.cat/${httpCat}`);
});
}
}否则,你的整个 Workflow 可能不如你想象的 durable,你可能会遇到一些未定义的行为。你可以通过遵循以下规则来避免这些问题:
- 🔴 不要将所有逻辑封装在单个步骤中。
- 🔴 不要在同一步骤中调用不同的服务(除非你需要这样做来证明幂等性)。
- 🔴 不要在同一步骤中进行过多服务调用(除非你需要这样做来证明幂等性)。
- 🔴 不要在单个步骤内进行过多 CPU 密集型工作——有时引擎可能需要重启,它会从该步骤的开头重新开始。
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// 🔴 Bad: you are calling two separate services from within the same step. This might cause
// some extra calls to the first service in case the second one fails, and in some cases, makes
// the step non-idempotent altogether
const image = await step.do("get cutest cat from KV", async () => {
const httpCat = await this.env.KV.get("cutest-http-cat");
return fetch(`https://http.cat/${httpCat}`);
});
}
}export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 🔴 Bad: you are calling two separate services from within the same step. This might cause
// some extra calls to the first service in case the second one fails, and in some cases, makes
// the step non-idempotent altogether
const image = await step.do("get cutest cat from KV", async () => {
const httpCat = await this.env.KV.get("cutest-http-cat");
return fetch(`https://http.cat/${httpCat}`);
});
}
}Workflows 可能会休眠并丢失所有内存中的状态。当引擎检测到没有待处理工作且可以休眠直到需要唤醒(由于 sleep、重试或事件)时,就会发生这种情况。
这意味着你不应在步骤之外存储状态:
function getRandomInt(min, max) {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// 🔴 Bad: `imageList` will be not persisted across engine's lifetimes. Which means that after hibernation,
// `imageList` will be empty again, even though the following two steps have already ran.
const imageList = [];
await step.do("get first cutest cat from KV", async () => {
const httpCat = await this.env.KV.get("cutest-http-cat-1");
imageList.push(httpCat);
});
await step.do("get second cutest cat from KV", async () => {
const httpCat = await this.env.KV.get("cutest-http-cat-2");
imageList.push(httpCat);
});
// A long sleep can (and probably will) hibernate the engine which means that the first engine lifetime ends here
await step.sleep("💤💤💤💤", "3 hours");
// When this runs, it will be on the second engine lifetime - which means `imageList` will be empty.
await step.do(
"choose a random cat from the list and download it",
async () => {
const randomCat = imageList.at(getRandomInt(0, imageList.length));
// this will fail since `randomCat` is undefined because `imageList` is empty
return await fetch(`https://http.cat/${randomCat}`);
},
);
}
}function getRandomInt(min, max) {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 🔴 Bad: `imageList` will be not persisted across engine's lifetimes. Which means that after hibernation,
// `imageList` will be empty again, even though the following two steps have already ran.
const imageList: string[] = [];
await step.do("get first cutest cat from KV", async () => {
const httpCat = await this.env.KV.get("cutest-http-cat-1");
imageList.push(httpCat);
});
await step.do("get second cutest cat from KV", async () => {
const httpCat = await this.env.KV.get("cutest-http-cat-2");
imageList.push(httpCat);
});
// A long sleep can (and probably will) hibernate the engine which means that the first engine lifetime ends here
await step.sleep("💤💤💤💤", "3 hours");
// When this runs, it will be on the second engine lifetime - which means `imageList` will be empty.
await step.do(
"choose a random cat from the list and download it",
async () => {
const randomCat = imageList.at(getRandomInt(0, imageList.length));
// this will fail since `randomCat` is undefined because `imageList` is empty
return await fetch(`https://http.cat/${randomCat}`);
},
);
}
}相反,你应该构建完全由 step.do 返回值组成的顶层状态:
function getRandomInt(min, max) {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// ✅ Good: imageList state is exclusively comprised of step returns - this means that in the event of
// multiple engine lifetimes, imageList will be built accordingly
const imageList = await Promise.all([
step.do("get first cutest cat from KV", async () => {
return await this.env.KV.get("cutest-http-cat-1");
}),
step.do("get second cutest cat from KV", async () => {
return await this.env.KV.get("cutest-http-cat-2");
}),
]);
// A long sleep can (and probably will) hibernate the engine which means that the first engine lifetime ends here
await step.sleep("💤💤💤💤", "3 hours");
// When this runs, it will be on the second engine lifetime - but this time, imageList will contain
// the two most cutest cats
await step.do(
"choose a random cat from the list and download it",
async () => {
const randomCat = imageList.at(getRandomInt(0, imageList.length));
// this will eventually succeed since `randomCat` is defined
return await fetch(`https://http.cat/${randomCat}`);
},
);
}
}function getRandomInt(min, max) {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// ✅ Good: imageList state is exclusively comprised of step returns - this means that in the event of
// multiple engine lifetimes, imageList will be built accordingly
const imageList: string[] = await Promise.all([
step.do("get first cutest cat from KV", async () => {
return await this.env.KV.get("cutest-http-cat-1");
}),
step.do("get second cutest cat from KV", async () => {
return await this.env.KV.get("cutest-http-cat-2");
}),
]);
// A long sleep can (and probably will) hibernate the engine which means that the first engine lifetime ends here
await step.sleep("💤💤💤💤", "3 hours");
// When this runs, it will be on the second engine lifetime - but this time, imageList will contain
// the two most cutest cats
await step.do(
"choose a random cat from the list and download it",
async () => {
const randomCat = imageList.at(getRandomInt(0, imageList.length));
// this will eventually succeed since `randomCat` is defined
return await fetch(`https://http.cat/${randomCat}`);
},
);
}
}不建议在步骤之外编写具有任何副作用的代码,除非你希望它重复执行,因为 Workflow 引擎可能在实例运行时重启。如果引擎重启,步骤逻辑将被保留,但步骤之外的逻辑可能会重复执行。
例如,workflow 步骤之外的 console.log() 可能在引擎重启时导致日志打印两次。
但是,涉及不可序列化资源(如数据库连接)的逻辑应在步骤之外执行。由于 Workflows 实例生命周期的特性,step.do 之外的操作可能会重复执行多次。
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// 🔴 Bad: creating instances outside of steps
// This might get called more than once creating more instances than expected
const badInstance = await this.env.ANOTHER_WORKFLOW.create();
// 🔴 Bad: using non-deterministic functions outside of steps
// this will produce different results if the instance has to restart, different runs of the same instance
// might go through different paths
const badRandom = Math.random();
if (badRandom > 0) {
// do some stuff
}
// ⚠️ Warning: This log may happen many times
console.log("This might be logged more than once");
await step.do("do some stuff and have a log for when it runs", async () => {
// do some stuff
// this log will only appear once
console.log("successfully did stuff");
});
// ✅ Good: wrap non-deterministic function in a step
// after running successfully will not run again
const goodRandom = await step.do("create a random number", async () => {
return Math.random();
});
// ✅ Good: calls that have no side effects can be done outside of steps
// For Hyperdrive, create the connection inside each step instead of here.
const db = createDBConnection(this.env.DB_URL, this.env.DB_TOKEN);
// ✅ Good: run functions with side effects inside of a step
// after running successfully will not run again
const goodInstance = await step.do(
"good step that returns state",
async () => {
const instance = await this.env.ANOTHER_WORKFLOW.create();
return instance;
},
);
}
}export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 🔴 Bad: creating instances outside of steps
// This might get called more than once creating more instances than expected
const badInstance = await this.env.ANOTHER_WORKFLOW.create();
// 🔴 Bad: using non-deterministic functions outside of steps
// this will produce different results if the instance has to restart, different runs of the same instance
// might go through different paths
const badRandom = Math.random();
if (badRandom > 0) {
// do some stuff
}
// ⚠️ Warning: This log may happen many times
console.log("This might be logged more than once");
await step.do("do some stuff and have a log for when it runs", async () => {
// do some stuff
// this log will only appear once
console.log("successfully did stuff");
});
// ✅ Good: wrap non-deterministic function in a step
// after running successfully will not run again
const goodRandom = await step.do("create a random number", async () => {
return Math.random();
});
// ✅ Good: calls that have no side effects can be done outside of steps
// For Hyperdrive, create the connection inside each step instead of here.
const db = createDBConnection(this.env.DB_URL, this.env.DB_TOKEN);
// ✅ Good: run functions with side effects inside of a step
// after running successfully will not run again
const goodInstance = await step.do(
"good step that returns state",
async () => {
const instance = await this.env.ANOTHER_WORKFLOW.create();
return instance;
},
);
}
}传递给 Workflow run 方法的 event 是不可变的:你对事件所做的更改不会在步骤和/或 Workflow 重启之间持久化。
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// 🔴 Bad: Mutating the event
// This will not be persisted across steps and `event.payload` will
// take on its original value.
await step.do("bad step that mutates the incoming event", async () => {
let userData = await this.env.KV.get(event.payload.user);
event.payload = userData;
});
// ✅ Good: persist data by returning it as state from your step
// Use that state in subsequent steps
let userData = await step.do("good step that returns state", async () => {
return await this.env.KV.get(event.payload.user);
});
let someOtherData = await step.do(
"following step that uses that state",
async () => {
// Access to userData here
// Will always be the same if this step is retried
},
);
}
}interface MyEvent {
user: string;
data: string;
}
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<MyEvent>, step: WorkflowStep) {
// 🔴 Bad: Mutating the event
// This will not be persisted across steps and `event.payload` will
// take on its original value.
await step.do("bad step that mutates the incoming event", async () => {
let userData = await this.env.KV.get(event.payload.user);
event.payload = userData;
});
// ✅ Good: persist data by returning it as state from your step
// Use that state in subsequent steps
let userData = await step.do("good step that returns state", async () => {
return await this.env.KV.get(event.payload.user);
});
let someOtherData = await step.do(
"following step that uses that state",
async () => {
// Access to userData here
// Will always be the same if this step is retried
},
);
}
}步骤应确定性地命名(即不使用当前日期/时间、随机性等)。这确保其状态被缓存,并防止步骤不必要地重新运行。步骤名称在 Workflow 中充当「缓存键」。
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// 🔴 Bad: Naming the step non-deterministically prevents it from being cached
// This will cause the step to be re-run if subsequent steps fail.
await step.do(`step #1 running at: ${Date.now()}`, async () => {
let userData = await this.env.KV.get(event.payload.user);
// Do not mutate event.payload
event.payload = userData;
});
// ✅ Good: give steps a deterministic name.
// Return dynamic values in your state, or log them instead.
let state = await step.do("fetch user data from KV", async () => {
let userData = await this.env.KV.get(event.payload.user);
console.log(`fetched at ${Date.now()}`);
return userData;
});
// ✅ Good: steps that are dynamically named are constructed in a deterministic way.
// In this case, `catList` is a step output, which is stable, and `catList` is
// traversed in a deterministic fashion (no shuffles or random accesses) so,
// it's fine to dynamically name steps (e.g: create a step per list entry).
let catList = await step.do("get cat list from KV", async () => {
return await this.env.KV.get("cat-list");
});
for (const cat of catList) {
await step.do(`get cat: ${cat}`, async () => {
return await this.env.KV.get(cat);
});
}
}
}export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 🔴 Bad: Naming the step non-deterministically prevents it from being cached
// This will cause the step to be re-run if subsequent steps fail.
await step.do(`step #1 running at: ${Date.now()}`, async () => {
let userData = await this.env.KV.get(event.payload.user);
// Do not mutate event.payload
event.payload = userData;
});
// ✅ Good: give steps a deterministic name.
// Return dynamic values in your state, or log them instead.
let state = await step.do("fetch user data from KV", async () => {
let userData = await this.env.KV.get(event.payload.user);
console.log(`fetched at ${Date.now()}`);
return userData;
});
// ✅ Good: steps that are dynamically named are constructed in a deterministic way.
// In this case, `catList` is a step output, which is stable, and `catList` is
// traversed in a deterministic fashion (no shuffles or random accesses) so,
// it's fine to dynamically name steps (e.g: create a step per list entry).
let catList = await step.do("get cat list from KV", async () => {
return await this.env.KV.get("cat-list");
});
for (const cat of catList) {
await step.do(`get cat: ${cat}`, async () => {
return await this.env.KV.get(cat);
});
}
}
}Workflows 允许在 Promise.race() 或 Promise.any() 方法内使用步骤来实现并发步骤执行。但是,必须考虑一些事项。
由于 Workflows 实例生命周期的特性,并且 Promise 内的步骤将运行直到完成,第一次通过时返回的步骤可能不是实际缓存的步骤,因为步骤按名称缓存。
// helper sleep method
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// 🔴 Bad: The `Promise.race` is not surrounded by a `step.do`, which may cause undeterministic caching behavior.
const race_return = await Promise.race([
step.do("Promise first race", async () => {
await sleep(1000);
return "first";
}),
step.do("Promise second race", async () => {
return "second";
}),
]);
await step.sleep("Sleep step", "2 hours");
return await step.do("Another step", async () => {
// This step will return `first`, even though the `Promise.race` first returned `second`.
return race_return;
});
}
}// helper sleep method
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 🔴 Bad: The `Promise.race` is not surrounded by a `step.do`, which may cause undeterministic caching behavior.
const race_return = await Promise.race([
step.do("Promise first race", async () => {
await sleep(1000);
return "first";
}),
step.do("Promise second race", async () => {
return "second";
}),
]);
await step.sleep("Sleep step", "2 hours");
return await step.do("Another step", async () => {
// This step will return `first`, even though the `Promise.race` first returned `second`.
return race_return;
});
}
}为确保一致性,我们建议将 Promise.race() 或 Promise.any() 包裹在 step.do() 内,这将确保多次通过时的缓存一致性。
// helper sleep method
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// ✅ Good: The `Promise.race` is surrounded by a `step.do`, ensuring deterministic caching behavior.
const race_return = await step.do("Promise step", async () => {
return await Promise.race([
step.do("Promise first race", async () => {
await sleep(1000);
return "first";
}),
step.do("Promise second race", async () => {
return "second";
}),
]);
});
await step.sleep("Sleep step", "2 hours");
return await step.do("Another step", async () => {
// This step will return `second` because the `Promise.race` was surround by the `step.do` method.
return race_return;
});
}
}// helper sleep method
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// ✅ Good: The `Promise.race` is surrounded by a `step.do`, ensuring deterministic caching behavior.
const race_return = await step.do("Promise step", async () => {
return await Promise.race([
step.do("Promise first race", async () => {
await sleep(1000);
return "first";
}),
step.do("Promise second race", async () => {
return "second";
}),
]);
});
await step.sleep("Sleep step", "2 hours");
return await step.do("Another step", async () => {
// This step will return `second` because the `Promise.race` was surround by the `step.do` method.
return race_return;
});
}
}Workflow 实例 ID 在每个 Workflow 中是唯一的。ID 是唯一标识符,将特定实例的日志、指标、状态和运行状态关联起来,即使在完成后也是如此。允许 ID 重用将难以理解 Workflow 实例 ID 是指昨天、上周还是今天运行的实例。
如果你希望为同一用户 ID 运行多个具有不同输入参数的不同 Workflow 实例,这也将带来问题,因为你将需要立即确定新的 ID 映射。
如果你需要将多个实例与系统中的特定用户、商户或其他「客户」ID 关联,请考虑使用复合 ID 或使用随机生成的 ID 并将映射存储在 D1 等数据库中。
// This is in the same file as your Workflow definition
export default {
async fetch(req, env) {
// 🔴 Bad: Use an ID that isn't unique across future Workflow invocations
let userId = getUserId(req); // Returns the userId
let badInstance = await env.MY_WORKFLOW.create({
id: userId,
params: payload,
});
// ✅ Good: use an ID that is unique
// e.g. a transaction ID, order ID, or task ID are good options
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.
instanceId = `${getUserId(req)}-${crypto.randomUUID().slice(0, 6)}`;
let { result } = await addNewInstanceToDB(userId, instanceId);
let goodInstance = await env.MY_WORKFLOW.create({
id: instanceId,
params: payload,
});
return Response.json({
id: goodInstance.id,
details: await goodInstance.status(),
});
},
};// 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 badInstance = await env.MY_WORKFLOW.create({
id: userId,
params: payload,
});
// ✅ Good: use an ID that is unique
// e.g. a transaction ID, order ID, or task ID are good options
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.
instanceId = `${getUserId(req)}-${crypto.randomUUID().slice(0, 6)}`;
let { result } = await addNewInstanceToDB(userId, instanceId);
let goodInstance = await env.MY_WORKFLOW.create({
id: instanceId,
params: payload,
});
return Response.json({
id: goodInstance.id,
details: await goodInstance.status(),
});
},
};调用 step.do 或 step.sleep 时,使用 await 以避免在 Workflow 代码中引入 bug 和竞态条件。
如果不调用 await step.do 或 await step.sleep,你会创建一个悬空 Promise。当 Promise 被创建但未正确 await 时会发生这种情况,导致潜在的 bug 和竞态条件。
当你不使用 await 关键字或未链式调用 .then() 方法来处理 Promise 结果时,就会发生这种情况。例如,调用 fetch(GITHUB_URL) 而不等待其响应将导致后续代码立即执行,无论 fetch 是否完成。这可能导致过早日志记录、异常被吞没(且不终止 Workflow)以及丢失返回值(状态)等问题。
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// 🔴 Bad: The step isn't await'ed, and any state or errors is swallowed before it returns.
const badIssues = step.do(`fetch issues from GitHub`, async () => {
// The step will return before this call is done
let issues = await getIssues(event.payload.repoName);
return issues;
});
// ✅ Good: The step is correctly await'ed.
const goodIssues = await step.do(`fetch issues from GitHub`, async () => {
let issues = await getIssues(event.payload.repoName);
return issues;
});
// Rest of your Workflow goes here!
}
}export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 🔴 Bad: The step isn't await'ed, and any state or errors is swallowed before it returns.
const badIssues = step.do(`fetch issues from GitHub`, async () => {
// The step will return before this call is done
let issues = await getIssues(event.payload.repoName);
return issues;
});
// ✅ Good: The step is correctly await'ed.
const goodIssues = await step.do(`fetch issues from GitHub`, async () => {
let issues = await getIssues(event.payload.repoName);
return issues;
});
// Rest of your Workflow goes here!
}
}你可以在步骤之外使用 if 语句、循环和其他控制流。但是,条件必须基于确定性值——来自 event.payload 的值或先前步骤的返回值。步骤之外基于非确定性条件(如 Math.random() 或 Date.now())的条件在 Workflow 重启时可能导致意外行为。
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const config = await step.do("fetch config", async () => {
return await this.env.KV.get("feature-flags", { type: "json" });
});
// ✅ Good: Condition based on step output (deterministic)
if (config.enableEmailNotifications) {
await step.do("send email", async () => {
// Send email logic
});
}
// ✅ Good: Condition based on event payload (deterministic)
if (event.payload.userType === "premium") {
await step.do("premium processing", async () => {
// Premium-only logic
});
}
// 🔴 Bad: Condition based on non-deterministic value outside a step
// This could behave differently if the Workflow restarts
if (Math.random() > 0.5) {
await step.do("maybe do something", async () => {});
}
// ✅ Good: Wrap non-deterministic values in a step
const shouldProcess = await step.do("decide randomly", async () => {
return Math.random() > 0.5;
});
if (shouldProcess) {
await step.do("conditionally do something", async () => {});
}
}
}export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const config = await step.do("fetch config", async () => {
return await this.env.KV.get("feature-flags", { type: "json" });
});
// ✅ Good: Condition based on step output (deterministic)
if (config.enableEmailNotifications) {
await step.do("send email", async () => {
// Send email logic
});
}
// ✅ Good: Condition based on event payload (deterministic)
if (event.payload.userType === "premium") {
await step.do("premium processing", async () => {
// Premium-only logic
});
}
// 🔴 Bad: Condition based on non-deterministic value outside a step
// This could behave differently if the Workflow restarts
if (Math.random() > 0.5) {
await step.do("maybe do something", async () => {});
}
// ✅ Good: Wrap non-deterministic values in a step
const shouldProcess = await step.do("decide randomly", async () => {
return Math.random() > 0.5;
});
if (shouldProcess) {
await step.do("conditionally do something", async () => {});
}
}
}创建多个 Workflow 实例时,使用 createBatch 方法将调用批量处理。这允许你在单个请求中创建多个 Workflow 实例,从而减少向 Workflows API 发出的请求数量。但是,批次中的每个单独实例仍将计入创建速率限制。与 create 不同,createBatch 是幂等的:如果具有相同 ID 的现有实例仍在其保留限制内,它将被跳过并从返回数组中排除。
export default {
async fetch(req, env) {
let instances = [
{ id: "user1", params: { name: "John" } },
{ id: "user2", params: { name: "Jane" } },
{ id: "user3", params: { name: "Alice" } },
{ id: "user4", params: { name: "Bob" } },
];
// 🔴 Bad: Create them one by one, which is more likely to hit creation rate limits.
for (let instance of instances) {
await env.MY_WORKFLOW.create({
id: instance.id,
params: instance.params,
});
}
// ✅ Good: Batch calls together
// This improves throughput.
let createdInstances = await env.MY_WORKFLOW.createBatch(instances);
return Response.json({ instances: createdInstances });
},
};export default {
async fetch(req: Request, env: Env): Promise<Response> {
let instances = [
{ id: "user1", params: { name: "John" } },
{ id: "user2", params: { name: "Jane" } },
{ id: "user3", params: { name: "Alice" } },
{ id: "user4", params: { name: "Bob" } },
];
// 🔴 Bad: Create them one by one, which is more likely to hit creation rate limits.
for (let instance of instances) {
await env.MY_WORKFLOW.create({
id: instance.id,
params: instance.params,
});
}
// ✅ Good: Batch calls together
// This improves throughput.
let createdInstances = await env.MY_WORKFLOW.createBatch(instances);
return Response.json({ instances: createdInstances });
},
};设置 WorkflowStep 超时 时,确保其持续时间为 30 分钟或更短。如果你的用例需要超过 30 分钟的超时,请考虑改用 step.waitForEvent()。
非流式 step.do() 返回值最多可持久化 1 MiB(2^20 字节)。如果你的步骤返回超过此限制的结构化数据,步骤将失败。这在获取大型 API 响应或处理大型文件时很常见。
在 JavaScript Workflows 中,ReadableStream<Uint8Array> 是用于较大二进制输出的支持的可序列化返回类型。持久化此类输出时,你应该:
-
从步骤回调返回新流。
-
将单个块保持在 16 MB 以下。
-
不要返回已锁定的流或已被读取的流。
-
仅依赖从步骤返回的流。
请注意,流式输出仍计入 Workflow 实例存储限制。
如果这些存储限制仍不能满足你的需求,请考虑将步骤输出外部存储(例如在 R2 中)并保存对其的引用。
export class MyWorkflow extends WorkflowEntrypoint {
async run(event, step) {
// 🔴 Bad: Returning a large response that may exceed 1 MiB
const largeData = await step.do("fetch large dataset", async () => {
const response = await fetch("https://api.example.com/large-dataset");
return await response.json(); // Could exceed 1 MiB
});
// ✅ Good: Store large structured data externally and return a reference
const dataRef = await step.do("fetch and store large dataset", async () => {
const response = await fetch("https://api.example.com/large-dataset");
const data = await response.json();
// Store in R2 and return a reference
await this.env.MY_BUCKET.put("dataset-123", JSON.stringify(data));
return { key: "dataset-123" };
});
// Retrieve the data in a later step when needed
const data = await step.do("process dataset", async () => {
const stored = await this.env.MY_BUCKET.get(dataRef.key);
return processData(await stored.json());
});
}
}export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 🔴 Bad: Returning a large response that may exceed 1 MiB
const largeData = await step.do("fetch large dataset", async () => {
const response = await fetch("https://api.example.com/large-dataset");
return await response.json(); // Could exceed 1 MiB
});
// ✅ Good: Store large structured data externally and return a reference
const dataRef = await step.do("fetch and store large dataset", async () => {
const response = await fetch("https://api.example.com/large-dataset");
const data = await response.json();
// Store in R2 and return a reference
await this.env.MY_BUCKET.put("dataset-123", JSON.stringify(data));
return { key: "dataset-123" };
});
// Retrieve the data in a later step when needed
const data = await step.do("process dataset", async () => {
const stored = await this.env.MY_BUCKET.get(dataRef.key);
return processData(await stored.json());
});
}
}- Workers 最佳实践:适用于触发 Workflows 的 Workers 的请求处理、可观测性和安全性的代码模式。
- Durable Objects 设计准则:有状态、协调应用的最佳实践——在将 Durable Objects 与 Workflows 结合使用时很有用。