Cookbook: Content Approval Workflow
This recipe demonstrates how to build a practical, multi-step content approval workflow. A new blog post is submitted, a manager is notified to approve it, and depending on the decision, the post is either published or sent back to the author.
This example combines several core concepts:
- A webhook trigger
- Custom nodes for business logic
- Pausing for human-in-the-loop
- Conditional branching
The Scenario
- A user submits a new blog post via a frontend application.
- The frontend sends a webhook to our application to start the workflow.
- The workflow saves the draft post to a database, marking its status as
pending_approval. - It then pauses, waiting for a manager's decision.
- A manager, via a separate UI, approves or rejects the post. This action resumes the workflow, providing the decision as a payload.
- If approved, a node updates the post's status to
published. - If rejected, a different node sends an email/notification back to the author.
Visualizing the Workflow
1. Node Definitions
We'll need several custom nodes for this.
typescript
import { object, string, boolean } from "valibot";
// Assume `db` and `email` are your own imported service clients.
const nodeDefinitions = {
"webhook-trigger": {
// A generic trigger node
executor: async (_, __, payload) => ({ data: payload || {} }),
},
"save-draft": {
input: object({ authorId: string(), title: string(), content: string() }),
executor: async (data) => {
const post = await db.posts.create({ ...data, status: "pending_approval" });
return { data: { postId: post.id } };
},
},
"wait-for-approval": {
// This node pauses if no decision is provided in the payload
executor: async (_, __, payload) => {
if (!payload || typeof (payload as any).approved !== 'boolean') {
return { data: {}, __pause: true };
}
const { approved } = payload as { approved: boolean };
return {
data: { decision: approved ? "approved" : "rejected" },
nextHandle: approved ? "approved" : "rejected",
};
},
},
"publish-post": {
input: object({ postId: string() }),
executor: async (data) => {
await db.posts.update({ where: { id: data.postId }, data: { status: "published" } });
return { data: { published: true } };
},
},
"notify-author": {
input: object({ authorId: string() }),
executor: async (data) => {
await email.send({ to: data.authorId, message: "Your post was rejected." });
return { data: { notified: true } };
},
},
};2. Definição do Workflow
The workflow wires these nodes together, using expressions to pass data between them.
typescript
const workflow: WorkflowDefinition = {
nodes: [
{
id: "trigger",
type: "webhook-trigger",
data: {},
},
{
id: "save",
type: "save-draft",
// Data comes from the initial webhook payload
data: {
authorId: "{{ trigger.last.data.authorId }}",
title: "{{ trigger.last.data.title }}",
content: "{{ trigger.last.data.content }}",
},
},
{
id: "approval",
type: "wait-for-approval",
data: {},
},
{
id: "publish",
type: "publish-post",
// The postId comes from the 'save' node's output
data: { postId: "{{ save.last.data.postId }}" },
},
{
id: "notify",
type: "notify-author",
// The authorId also comes from the initial payload
data: { authorId: "{{ trigger.last.data.authorId }}" },
},
],
edges: [
{ source: "trigger", target: "save" },
{ source: "save", target: "approval" },
{ source: "approval", target: "publish", sourceHandle: "approved" },
{ source: "approval", target: "notify", sourceHandle: "rejected" },
],
};This recipe shows how you can combine simple, single-purpose nodes into a sophisticated, resilient, and long-running business process.