INTEGRITY Dokumentace

Pravidla pro Workflows

Workflow obsahuje jeden nebo více kroků. Každý krok je samostatná, jednotlivě opakovatelná součást Workflow. Kroky mohou volitelně předávat stav, díky kterému Workflow dokáže uložit svůj postup a pokračovat od daného kroku i v případě, že Workflow selže kvůli problému se sítí nebo infrastrukturou.

Toto je krátká příručka, jak vytvářet odolnější a správně fungující Workflows.

Zajistěte, aby volání API/Binding byla idempotentní

Protože se krok může opakovat vícekrát, měly by být vaše kroky (ideálně) idempotentní. Idempotence je vlastnost, kdy operaci (v tomto případě krok) lze provést opakovaně, aniž by se výsledek po prvním provedení dále měnil.

Jako příklad předpokládejme, že máte Workflow, který zákazníkům účtuje platby, a vy je určitě nechcete omylem naúčtovat dvakrát. Před naúčtováním byste měli zkontrolovat, zda jim již nebylo účtováno:

index.js
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 }),
					},
				);
			},
		);
	}
}
index.ts
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 }),
					},
				);
			},
		);
	}
}

Udržujte kroky granulární

Kroky by měly být co nejvíce samostatné. Díky tomu bude vaše vlastní logika odolnější vůči selháním API třetích stran, síťovým chybám a podobným problémům.

Můžete si to představit také jako transakci nebo jednotku práce.

index.js
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}`);
		});
	}
}
index.ts
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}`);
		});
	}
}

Jinak nemusí být celý váš Workflow tak odolný, jak si myslíte, a můžete narazit na nedefinované chování. Tomu se vyhnete dodržováním následujících pravidel:

index.js
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}`);
		});
	}
}
index.ts
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}`);
		});
	}
}

Nespoléhejte se na stav mimo krok

Workflows mohou přejít do režimu hibernace a ztratit veškerý stav uložený v paměti. Stává se to ve chvíli, kdy engine zjistí, že nezbývá žádná čekající práce, a hibernuje, dokud není potřeba se znovu probudit (kvůli sleep, retry nebo události).

To znamená, že stav byste neměli ukládat mimo krok:

index.js
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}`);
			},
		);
	}
}
index.ts
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}`);
			},
		);
	}
}

Místo toho byste měli stav nejvyšší úrovně sestavit výhradně z step.do vrací:

index.js
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}`);
			},
		);
	}
}
index.ts
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}`);
			},
		);
	}
}

Vyhněte se vedlejším efektům mimo step.do

Nedoporučuje se psát kód s vedlejšími efekty mimo kroky, pokud si nepřejete, aby se prováděl opakovaně, protože engine Workflow může za běhu instance restartovat. Po restartu enginu zůstane logika kroků zachována, ale logika mimo kroky se může zdvojit.

Například console.log() mimo kroky Workflow může při restartu enginu způsobit, že se logy vypíšou dvakrát.

Logika zahrnující neserializovatelné prostředky, jako je připojení k databázi, by se však měla provádět mimo kroky. Operace mimo step.do se může kvůli povaze životního cyklu instancí Workflows opakovat vícekrát.

index.js
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;
			},
		);
	}
}
index.ts
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;
			},
		);
	}
}

Neupravujte příchozí události

event předaný metodě vašeho Workflow run metoda je neměnná: změny, které provedete v události, se neuchovávají mezi kroky ani při restartech Workflow.

index.js
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
			},
		);
	}
}
index.ts
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
			},
		);
	}
}

Pojmenovávejte kroky deterministicky

Kroky pojmenovávejte deterministicky, to znamená nepoužívejte aktuální datum a čas, náhodné hodnoty a podobně. Jejich stav se tak ukládá do mezipaměti a krok se zbytečně neopakuje. Název kroku totiž slouží jako „klíč mezipaměti“ ve vašem Workflow.

index.js
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);
			});
		}
	}
}
index.ts
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);
			});
		}
	}
}

Dávejte pozor na Promise.race() a Promise.any()

Workflows umožňují použití kroků v rámci Promise.race() nebo Promise.any() metody jako způsob dosažení souběžného provádění kroků. Je však třeba zvážit několik okolností.

Vzhledem k povaze životního cyklu instancí Workflows a k tomu, že krok uvnitř Promise poběží až do svého dokončení, nemusí být krok vrácený při prvním průchodu ve skutečnosti tím uloženým krokem v mezipaměti, protože kroky se ukládají do mezipaměti podle svých názvů.

index.js
// 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;
		});
	}
}
index.ts
// 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;
		});
	}
}

Pro zajištění konzistence doporučujeme obalit Promise.race() nebo Promise.any() uvnitř step.do(), protože to zajistí konzistenci cachování napříč více pasážemi.

index.js
// 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;
		});
	}
}
index.ts
// 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;
		});
	}
}

ID instancí jsou jedinečná

Workflow ID instancí jsou jedinečná v rámci Workflow. ID je jedinečný identifikátor, který propojuje protokoly, metriky, stav a status běhu s konkrétní instancí, a to i po jeho dokončení. Kdyby bylo možné ID používat opakovaně, bylo by obtížné určit, zda se ID instance Workflow vztahuje k instanci spuštěné včera, minulý týden nebo dnes.

Problém by nastal také v případě, že byste chtěli spustit více různých instancí Workflow s různými vstupní parametry pro stejné ID uživatele, protože byste okamžitě potřebovali určit nové mapování ID.

Pokud potřebujete přiřadit více instancí ke konkrétnímu uživateli, obchodníkovi nebo jinému ID "zákazníka" ve vašem systému, zvažte použití složeného ID nebo náhodně generovaných ID s uložením mapování do databáze, jako je D1.

index.js
// 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(),
		});
	},
};
index.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 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(),
		});
	},
};

await vaše kroky

Při volání step.do nebo step.sleep, použijte await abyste do kódu svého Workflow nezanesli chyby a race conditions.

Pokud nezavoláte await step.do nebo await step.sleep, vytvoříte visící Promise. K tomu dochází, když je Promise vytvořen, ale není správně await, což může vést k chybám a race conditions.

K tomu dochází, když nepoužijete await klíčové slovo nebo nepoužijete zřetězení .then() metody pro zpracování výsledku Promise. Například zavolání fetch(GITHUB_URL) bez čekání na odpověď způsobí, že se následující kód provede okamžitě bez ohledu na to, zda fetch doběhl. To může vést k problémům, jako je předčasné logování, pohlcení výjimek (které pak neukončí Workflow), nebo ke ztrátě návratových hodnot (stavu).

index.js
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!
	}
}
index.ts
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!
	}
}

Podmíněnou logiku používejte obezřetně

Můžete použít if příkazy, smyčky a další prvky řízení toku mimo kroky. Podmínky však musí vycházet z deterministické hodnoty, buď hodnoty z event.payload nebo návratové hodnoty z předchozích kroků. Nedeterministické podmínky (například Math.random() nebo Date.now()) mimo kroky může při restartu Workflow způsobit neočekávané chování.

index.js
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 () => {});
		}
	}
}
index.ts
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 () => {});
		}
	}
}

Hromadné zpracování více spuštění Workflow

Při vytváření více instancí Workflow použijte createBatch metodu ke sloučení volání do dávky. To vám umožní vytvořit více instancí Workflow v rámci jednoho požadavku, čímž se snižuje počet požadavků na API Workflows. Každá jednotlivá instance v dávce se však stále započítává do limit frekvence vytváření. Na rozdíl od create, createBatch je idempotentní: pokud existující instance se stejným ID stále spadá do jejího limit uchovávání, bude přeskočen a vyřazen z vráceného pole.

index.js
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 });
	},
};
index.ts
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 });
	},
};

Timeouty omezte na 30 minut nebo méně

Při nastavování Timeout u WorkflowStep, zajistěte, aby jeho délka byla 30 minut nebo kratší. Pokud váš případ použití vyžaduje delší časový limit než 30 minut, zvažte použití step.waitForEvent() místo toho.

Udržujte návratové hodnoty nestreamovaných kroků pod 1 MiB

Nestreamovaný step.do() návratová hodnota může trvale uložit až 1 MiB (2^20 bajtů). Pokud krok vrátí strukturovaná data přesahující tento limit, krok selže. Jde o častý problém při načítání velkých odpovědí API nebo zpracování velkých souborů.

V Workflows v JavaScriptu, ReadableStream<Uint8Array> je podporovaný serializovatelný návratový typ pro větší binární výstup. Při ukládání tohoto typu výstupu byste měli:

Upozorňujeme, že streamované výstupy se stále započítávají do limitu úložiště instance Workflow.

Pokud vám tyto limity úložiště přesto nevyhovují, zvažte externí ukládání výstupů kroků (například do R2) a uložení odkazu na něj.

index.js
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());
		});
	}
}
index.ts
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());
		});
	}
}