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
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
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.
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 NULL8 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);1314CREATE 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 NULL19 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);2324CREATE 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 timestamptz32);3334CREATE 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);4546CREATE 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.
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.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { Resend } from 'resend'45const 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)1011async 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?: string19 }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: string26 }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}6667export async function POST(req: NextRequest) {68 const { workflowId, triggerData } = await req.json()6970 const { data: run } = await supabase71 .from('workflow_runs')72 .insert({ workflow_id: workflowId, trigger_data: triggerData })73 .select()74 .single()7576 const { data: steps } = await supabase77 .from('workflow_steps')78 .select('*')79 .eq('workflow_id', workflowId)80 .order('position')8182 let context: Record<string, unknown> = { trigger: triggerData }83 let failed = false8485 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 }9091 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 = true101 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 }111112 await supabase113 .from('workflow_runs')114 .update({115 status: failed ? 'failed' : 'completed',116 completed_at: new Date().toISOString(),117 })118 .eq('id', run!.id)119120 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.
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.
1// app/api/webhooks/trigger/[id]/route.ts2import { NextRequest, NextResponse } from 'next/server'3import { createClient } from '@supabase/supabase-js'45const supabase = createClient(6 process.env.NEXT_PUBLIC_SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)910export async function POST(11 req: NextRequest,12 { params }: { params: Promise<{ id: string }> }13) {14 const { id } = await params15 const triggerData = await req.json()1617 const { data: workflow } = await supabase18 .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()2425 if (!workflow) {26 return NextResponse.json({ error: 'Workflow not found or inactive' }, { status: 404 })27 }2829 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 )3738 const result = await executeRes.json()39 return NextResponse.json(result)40}4142// vercel.json — add this to the project root43// { "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.
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.
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 component4// 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 canvas6// 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 body8// - send_email: Input for to/subject, Textarea for HTML template with {{variable}} placeholders9// - transform_data: Textarea for JavaScript expression10// - condition: Input for field, Select for operator, Input for value11// - delay: Input for milliseconds12// - supabase_query: Input for table, Select for operation, Textarea for filters/data JSON13// 5. Save Button that persists the node positions and step configs to Supabase14// 6. shadcn/ui Switch for workflow active toggle15// 7. Show the webhook URL (for webhook triggers) with copy-to-clipboard Button16// 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.
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.
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 DESC4// 2. shadcn/ui Table with columns: Run ID (truncated), Status Badge (running=secondary, completed=default, failed=destructive), Started At, Duration, Steps Passed/Total5// 3. Clicking a row expands an Accordion showing step_logs for that run6// 4. Each step log shows: step action_type Badge, status Badge, duration, input (collapsible JSON), output (collapsible JSON), error message if failed7// 5. Summary Cards at top: total runs, success rate percentage, average duration, runs today8// 6. Manual trigger Button that executes the workflow with an optional JSON input Dialog9// 7. Filter by status using Select dropdown10// 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
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function POST(req: NextRequest) {10 const { workflowId, triggerData } = await req.json()1112 const { data: run } = await supabase13 .from('workflow_runs')14 .insert({ workflow_id: workflowId, trigger_data: triggerData })15 .select()16 .single()1718 const { data: steps } = await supabase19 .from('workflow_steps')20 .select('*')21 .eq('workflow_id', workflowId)22 .order('position')2324 let context: Record<string, unknown> = { trigger: triggerData }25 let failed = false2627 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 }3233 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 = true43 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 }5455 await supabase56 .from('workflow_runs')57 .update({58 status: failed ? 'failed' : 'completed',59 completed_at: new Date().toISOString(),60 })61 .eq('id', run!.id)6263 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation