Skip to main content
RapidDev - Software Development Agency

How to Build Workflow automation with V0

Build a Zapier-lite workflow automation platform with V0 featuring a visual trigger-action builder using reactflow, webhook and cron triggers, sequential step execution with context passing, and detailed run logs. You'll configure Vercel Cron Jobs in vercel.json, build a reduce-style async pipeline, and log per-step results — all in about 2-4 hours.

What you'll build

  • Visual workflow builder using reactflow with drag-and-drop nodes for triggers and actions
  • Webhook trigger endpoint that accepts POST data and executes the associated workflow
  • Vercel Cron Jobs integration via vercel.json for scheduled workflow execution
  • Sequential step execution pipeline with context passing where each step's output feeds the next
  • Detailed run history with Accordion-expandable step logs showing input, output, and errors
  • Step configuration forms with Select for action type and dynamic Form fields per action
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced13 min read2-4 hoursV0 Premium (Supabase + Vercel Cron)April 2026RapidDev Engineering Team
TL;DR

Build a Zapier-lite workflow automation platform with V0 featuring a visual trigger-action builder using reactflow, webhook and cron triggers, sequential step execution with context passing, and detailed run logs. You'll configure Vercel Cron Jobs in vercel.json, build a reduce-style async pipeline, and log per-step results — all in about 2-4 hours.

What you're building

Workflow automation connects triggers (something happens) to actions (do something about it). A webhook receives data, a cron job fires on schedule, or a database change occurs — then a sequence of actions runs: send an email, call an API, transform data, update a database record.

V0 generates the workflow builder UI, trigger endpoints, and execution pipeline from prompts. Reactflow provides the visual node editor for building workflows. Supabase stores workflows, steps, run history, and per-step logs. Vercel Cron Jobs handle scheduled triggers natively.

The architecture uses an API route for webhook triggers, a cron route for scheduled execution, a pipeline endpoint that fetches steps in order and executes them sequentially with a shared context object, and Server Components for the workflow list and run history.

Final result

A workflow automation platform with visual builder, webhook and cron triggers, configurable action steps, sequential execution with context passing, and detailed run logs.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase & Auth
reactflowVisual Workflow Editor
ResendEmail Sending

Prerequisites

  • A V0 account (Premium recommended for the project complexity)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • A Resend account for the send-email action (free tier: 100 emails/day)
  • No additional services needed — Vercel Cron Jobs are built into the platform

Build steps

1

Set up the workflows, steps, runs, and logs schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Create the tables for workflow definitions, configurable steps, execution runs, and per-step logs.

supabase/migrations/001_schema.sql
1CREATE TABLE workflows (
2 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3 owner_id uuid NOT NULL,
4 name text NOT NULL,
5 description text,
6 is_active boolean DEFAULT false,
7 trigger_type text NOT NULL
8 CHECK (trigger_type IN ('webhook','schedule','db_change')),
9 trigger_config jsonb DEFAULT '{}',
10 created_at timestamptz DEFAULT now(),
11 updated_at timestamptz DEFAULT now()
12);
13
14CREATE TABLE workflow_steps (
15 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
16 workflow_id uuid REFERENCES workflows(id) ON DELETE CASCADE,
17 position int NOT NULL,
18 action_type text NOT NULL
19 CHECK (action_type IN ('http_request','send_email','transform_data','condition','delay','supabase_query')),
20 config jsonb NOT NULL DEFAULT '{}',
21 created_at timestamptz DEFAULT now()
22);
23
24CREATE TABLE workflow_runs (
25 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
26 workflow_id uuid REFERENCES workflows(id) ON DELETE CASCADE,
27 status text DEFAULT 'running'
28 CHECK (status IN ('running','completed','failed')),
29 trigger_data jsonb,
30 started_at timestamptz DEFAULT now(),
31 completed_at timestamptz
32);
33
34CREATE TABLE step_logs (
35 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
36 run_id uuid REFERENCES workflow_runs(id) ON DELETE CASCADE,
37 step_id uuid REFERENCES workflow_steps(id) ON DELETE CASCADE,
38 status text CHECK (status IN ('success','failed','skipped')),
39 input jsonb,
40 output jsonb,
41 error_message text,
42 duration_ms int,
43 executed_at timestamptz DEFAULT now()
44);
45
46CREATE INDEX idx_steps_workflow ON workflow_steps(workflow_id, position);
47CREATE INDEX idx_runs_workflow ON workflow_runs(workflow_id, started_at DESC);

Pro tip: The position column on workflow_steps determines execution order. Using integers with gaps (10, 20, 30) makes it easier to insert steps between existing ones without reordering.

Expected result: Four tables created with indexes for fast step ordering and run history queries. Step logs track input, output, and errors for each step in each run.

2

Build the workflow execution pipeline

Create the core execution engine that fetches a workflow's steps in order, runs them sequentially, and passes a context object from step to step. Each step's output is merged into the context for the next step.

app/api/workflows/execute/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3import { Resend } from 'resend'
4
5const supabase = createClient(
6 process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9const resend = new Resend(process.env.RESEND_API_KEY)
10
11async function executeStep(
12 step: { id: string; action_type: string; config: Record<string, unknown> },
13 context: Record<string, unknown>
14): Promise<Record<string, unknown>> {
15 switch (step.action_type) {
16 case 'http_request': {
17 const { url, method, headers, body } = step.config as {
18 url: string; method: string; headers?: Record<string, string>; body?: string
19 }
20 const res = await fetch(url, { method, headers, body })
21 return { status: res.status, data: await res.json() }
22 }
23 case 'send_email': {
24 const { to, subject, template } = step.config as {
25 to: string; subject: string; template: string
26 }
27 await resend.emails.send({
28 from: 'workflows@yourdomain.com',
29 to,
30 subject,
31 html: template.replace(/\{\{(\w+)\}\}/g, (_, key) =>
32 String(context[key] ?? '')
33 ),
34 })
35 return { sent: true, to }
36 }
37 case 'transform_data': {
38 const { expression } = step.config as { expression: string }
39 const fn = new Function('ctx', `return (${expression})`)
40 return { result: fn(context) }
41 }
42 case 'delay': {
43 const { ms } = step.config as { ms: number }
44 await new Promise((r) => setTimeout(r, Math.min(ms, 10000)))
45 return { delayed: ms }
46 }
47 case 'supabase_query': {
48 const { table, operation, filters, data } = step.config as {
49 table: string; operation: string; filters?: Record<string, unknown>; data?: Record<string, unknown>
50 }
51 let query = supabase.from(table)
52 if (operation === 'select') {
53 const { data: rows } = await query.select('*').match(filters ?? {})
54 return { rows }
55 }
56 if (operation === 'insert') {
57 await query.insert(data ?? {})
58 return { inserted: true }
59 }
60 return {}
61 }
62 default:
63 return {}
64 }
65}
66
67export async function POST(req: NextRequest) {
68 const { workflowId, triggerData } = await req.json()
69
70 const { data: run } = await supabase
71 .from('workflow_runs')
72 .insert({ workflow_id: workflowId, trigger_data: triggerData })
73 .select()
74 .single()
75
76 const { data: steps } = await supabase
77 .from('workflow_steps')
78 .select('*')
79 .eq('workflow_id', workflowId)
80 .order('position')
81
82 let context: Record<string, unknown> = { trigger: triggerData }
83 let failed = false
84
85 for (const step of steps ?? []) {
86 const start = Date.now()
87 try {
88 const output = await executeStep(step, context)
89 context = { ...context, [step.action_type]: output }
90
91 await supabase.from('step_logs').insert({
92 run_id: run!.id,
93 step_id: step.id,
94 status: 'success',
95 input: context,
96 output,
97 duration_ms: Date.now() - start,
98 })
99 } catch (err) {
100 failed = true
101 await supabase.from('step_logs').insert({
102 run_id: run!.id,
103 step_id: step.id,
104 status: 'failed',
105 input: context,
106 error_message: err instanceof Error ? err.message : 'Unknown error',
107 duration_ms: Date.now() - start,
108 })
109 }
110 }
111
112 await supabase
113 .from('workflow_runs')
114 .update({
115 status: failed ? 'failed' : 'completed',
116 completed_at: new Date().toISOString(),
117 })
118 .eq('id', run!.id)
119
120 return NextResponse.json({ runId: run!.id, status: failed ? 'failed' : 'completed' })
121}

Pro tip: The context object accumulates through the pipeline. Each step can read previous steps' output via context.http_request.data or context.trigger.fieldName. Failed steps log their error but do not halt subsequent steps.

Expected result: The execute endpoint runs all steps sequentially, logs each step's input/output/duration, and marks the run as completed or failed.

3

Create webhook and cron trigger endpoints

Build the webhook trigger that accepts POST data and the cron trigger that checks for scheduled workflows. Configure Vercel Cron Jobs in vercel.json.

app/api/webhooks/trigger/[id]/route.ts
1// app/api/webhooks/trigger/[id]/route.ts
2import { NextRequest, NextResponse } from 'next/server'
3import { createClient } from '@supabase/supabase-js'
4
5const supabase = createClient(
6 process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9
10export async function POST(
11 req: NextRequest,
12 { params }: { params: Promise<{ id: string }> }
13) {
14 const { id } = await params
15 const triggerData = await req.json()
16
17 const { data: workflow } = await supabase
18 .from('workflows')
19 .select('id, is_active, trigger_type')
20 .eq('id', id)
21 .eq('trigger_type', 'webhook')
22 .eq('is_active', true)
23 .single()
24
25 if (!workflow) {
26 return NextResponse.json({ error: 'Workflow not found or inactive' }, { status: 404 })
27 }
28
29 const executeRes = await fetch(
30 `${process.env.NEXT_PUBLIC_APP_URL}/api/workflows/execute`,
31 {
32 method: 'POST',
33 headers: { 'Content-Type': 'application/json' },
34 body: JSON.stringify({ workflowId: id, triggerData }),
35 }
36 )
37
38 const result = await executeRes.json()
39 return NextResponse.json(result)
40}
41
42// vercel.json — add this to the project root
43// { "crons": [{ "path": "/api/cron", "schedule": "*/5 * * * *" }] }

Pro tip: Vercel Cron Jobs are configured in vercel.json and run automatically in production. The cron route should query for active workflows with trigger_type='schedule' and matching cron expressions, then call the execute endpoint for each.

Expected result: Webhook triggers accept POST data and execute the workflow. Cron triggers run every 5 minutes, checking for due scheduled workflows.

4

Build the visual workflow builder with reactflow

Create the workflow editor page using reactflow for visual node editing. Users drag trigger and action nodes into a canvas, configure each step, and connect them to define the execution flow.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a visual workflow builder at app/workflows/[id]/page.tsx with:
3// 1. Install reactflow and add it as a client component
4// 2. Custom node types: TriggerNode (webhook/schedule/db_change with config), ActionNode (http_request/send_email/transform_data/condition/delay/supabase_query)
5// 3. Sidebar with draggable action types that can be dropped onto the canvas
6// 4. Clicking a node opens a Dialog with a dynamic Form based on action_type:
7// - http_request: Input for URL, Select for method, Textarea for headers JSON, Textarea for body
8// - send_email: Input for to/subject, Textarea for HTML template with {{variable}} placeholders
9// - transform_data: Textarea for JavaScript expression
10// - condition: Input for field, Select for operator, Input for value
11// - delay: Input for milliseconds
12// - supabase_query: Input for table, Select for operation, Textarea for filters/data JSON
13// 5. Save Button that persists the node positions and step configs to Supabase
14// 6. shadcn/ui Switch for workflow active toggle
15// 7. Show the webhook URL (for webhook triggers) with copy-to-clipboard Button
16// Use reactflow's built-in edges for connecting nodes.

Expected result: A visual canvas where users drag action nodes, connect them to a trigger, configure each step via Dialog forms, and save the workflow to Supabase.

5

Build the run history and step logs page

Create a page showing all runs for a workflow with expandable step-by-step logs showing input, output, duration, and error messages.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a workflow run history page at app/workflows/[id]/runs/page.tsx with:
3// 1. Server Component fetching all workflow_runs for the workflow, ordered by started_at DESC
4// 2. shadcn/ui Table with columns: Run ID (truncated), Status Badge (running=secondary, completed=default, failed=destructive), Started At, Duration, Steps Passed/Total
5// 3. Clicking a row expands an Accordion showing step_logs for that run
6// 4. Each step log shows: step action_type Badge, status Badge, duration, input (collapsible JSON), output (collapsible JSON), error message if failed
7// 5. Summary Cards at top: total runs, success rate percentage, average duration, runs today
8// 6. Manual trigger Button that executes the workflow with an optional JSON input Dialog
9// 7. Filter by status using Select dropdown
10// Use pre/code blocks for JSON display with syntax highlighting.

Expected result: A run history page with Table of runs, expandable Accordion for step logs, status Badges, and summary Cards showing execution statistics.

Complete code

app/api/workflows/execute/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function POST(req: NextRequest) {
10 const { workflowId, triggerData } = await req.json()
11
12 const { data: run } = await supabase
13 .from('workflow_runs')
14 .insert({ workflow_id: workflowId, trigger_data: triggerData })
15 .select()
16 .single()
17
18 const { data: steps } = await supabase
19 .from('workflow_steps')
20 .select('*')
21 .eq('workflow_id', workflowId)
22 .order('position')
23
24 let context: Record<string, unknown> = { trigger: triggerData }
25 let failed = false
26
27 for (const step of steps ?? []) {
28 const start = Date.now()
29 try {
30 const output = await executeStep(step, context)
31 context = { ...context, [`step_${step.position}`]: output }
32
33 await supabase.from('step_logs').insert({
34 run_id: run!.id,
35 step_id: step.id,
36 status: 'success',
37 input: context,
38 output,
39 duration_ms: Date.now() - start,
40 })
41 } catch (err) {
42 failed = true
43 await supabase.from('step_logs').insert({
44 run_id: run!.id,
45 step_id: step.id,
46 status: 'failed',
47 input: context,
48 error_message:
49 err instanceof Error ? err.message : 'Unknown error',
50 duration_ms: Date.now() - start,
51 })
52 }
53 }
54
55 await supabase
56 .from('workflow_runs')
57 .update({
58 status: failed ? 'failed' : 'completed',
59 completed_at: new Date().toISOString(),
60 })
61 .eq('id', run!.id)
62
63 return NextResponse.json({
64 runId: run!.id,
65 status: failed ? 'failed' : 'completed',
66 })
67}

Customization ideas

Add conditional branching

Implement an if/else node that evaluates a condition against the context and routes execution to different branches. Use reactflow's edge handles to create split paths.

Add retry logic for failed steps

Configure per-step retry count and delay. When a step fails, retry it up to N times with exponential backoff before marking it as failed.

Add workflow templates

Create pre-built workflow templates (e.g., 'Notify on form submission', 'Daily report email') that users can clone and customize instead of building from scratch.

Add Supabase Realtime trigger

Use Supabase Realtime to listen for database changes (INSERT, UPDATE, DELETE) on specific tables and trigger workflows when matching events occur.

Add workflow versioning

Save each workflow edit as a new version. Allow users to view version history, compare changes, and rollback to previous versions.

Common pitfalls

Pitfall: Running workflow steps in parallel instead of sequentially

How to avoid: Use a sequential for...of loop (not Promise.all) to execute steps one at a time. Each step's output is merged into the context object before the next step runs.

Pitfall: Not logging step inputs and outputs

How to avoid: Log the full context (input), step output, duration, and any error message to the step_logs table for every step execution, whether it succeeds or fails.

Pitfall: Using Vercel Cron with test URLs instead of production

How to avoid: Test scheduled workflows manually by calling the cron endpoint directly during development. Vercel Cron Jobs activate automatically on the production deployment.

Pitfall: Not capping the delay action duration

How to avoid: Cap the delay action at 10 seconds (Math.min(ms, 10000)). For longer delays, split the workflow into two workflows where the first triggers the second after a schedule.

Best practices

  • Execute workflow steps sequentially using a for...of loop with a shared context object that accumulates each step's output
  • Log every step execution to step_logs with input, output, duration, and error details for debugging
  • Configure Vercel Cron Jobs in vercel.json for scheduled triggers — this is a native Vercel capability that requires no third-party service
  • Cap delay actions at 10 seconds to stay within Vercel serverless timeout limits
  • Use V0's prompt queuing to build the workflow list, visual builder, and run history as three queued prompts
  • Set RESEND_API_KEY in V0's Vars tab (server-only, no NEXT_PUBLIC_ prefix) for the send-email action
  • Generate unique webhook URLs per workflow using the deployed Vercel domain so each workflow has its own trigger endpoint

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a workflow automation platform with Next.js App Router and Supabase. I need: 1) A sequential step execution pipeline where each step's output becomes the next step's input via a shared context object, 2) Webhook trigger endpoints per workflow, 3) Vercel Cron Jobs configuration for scheduled triggers, 4) Per-step logging with input/output/duration/errors. Help me design the execution engine and data model.

Build Prompt

Create a workflow execution API at app/api/workflows/execute/route.ts that: 1) Creates a workflow_run record, 2) Fetches workflow_steps ordered by position, 3) Executes steps sequentially using a for...of loop, 4) Maintains a context object where each step's output is merged in, 5) Logs every step to step_logs with input/output/duration/error, 6) Updates the run status to completed or failed. Support action types: http_request (fetch), send_email (Resend), transform_data (function evaluation), delay (setTimeout), supabase_query (CRUD).

Frequently asked questions

How does context passing work between steps?

The execution engine maintains a context object initialized with { trigger: triggerData }. After each step executes, its output is merged into the context (e.g., context.step_10 = { status: 200, data: {...} }). The next step receives the full accumulated context, so it can reference any previous step's output.

How do Vercel Cron Jobs work?

Add a crons array to vercel.json: { "crons": [{ "path": "/api/cron", "schedule": "*/5 * * * *" }] }. Vercel calls the specified path on the production deployment at the schedule interval. The cron route queries for active scheduled workflows and triggers their execution.

Can I test cron workflows locally?

Vercel Cron Jobs only run in production. For local testing, call the /api/cron endpoint directly via curl or the browser. You can also add a manual trigger Button on the workflow page that calls the execute endpoint with test data.

What V0 plan do I need?

V0 Premium ($20/month) is recommended. The workflow builder involves reactflow integration, multiple API routes, and a complex dashboard that requires several prompt iterations. Vercel Cron Jobs work on all Vercel plans including Hobby.

What happens when a step fails?

The failed step is logged with its error message and the context at the time of failure. Subsequent steps continue executing (the pipeline does not halt). The overall run is marked as 'failed' if any step fails. This allows partial workflow completion while making failures visible in the logs.

How do I deploy this?

Click Share then Publish in V0. Set RESEND_API_KEY in V0's Vars tab (no NEXT_PUBLIC_ prefix). Ensure vercel.json contains the crons configuration. After deployment, webhook trigger URLs are available at https://your-domain.vercel.app/api/webhooks/trigger/{workflow-id}.

Can RapidDev help build a custom automation platform?

Yes. RapidDev has built 600+ apps including workflow automation platforms with complex trigger systems, multi-step pipelines, and integration layers. Book a free consultation to discuss your automation requirements.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.