Skip to main content
RapidDev - Software Development Agency

How to Build a Workflow Automation with Lovable

Build a visual workflow automation engine in Lovable where workflows are defined as a DAG of steps stored in JSONB. An Edge Function execution engine traverses the DAG, runs each step's action (HTTP call, database query, email, condition branch), and records the full run history. Webhook and cron triggers fire workflows automatically.

What you'll build

  • A workflows table with steps DAG stored as JSONB defining nodes and their connections
  • An execution engine Edge Function that traverses and runs each step type sequentially
  • Support for four step types: HTTP request, Supabase query, condition branch, and delay
  • A workflow_runs table logging each execution with step-level traces
  • A webhook trigger endpoint and a pg_cron trigger for scheduled workflows
  • A visual step-list builder UI showing the workflow as connected cards
  • A run history viewer with step-by-step execution trace and output data
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced17 min read4–5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a visual workflow automation engine in Lovable where workflows are defined as a DAG of steps stored in JSONB. An Edge Function execution engine traverses the DAG, runs each step's action (HTTP call, database query, email, condition branch), and records the full run history. Webhook and cron triggers fire workflows automatically.

What you're building

A workflow is a directed acyclic graph (DAG) of steps. Each step has a type, configuration, and one or more connections to the next steps. The steps JSONB stores this as: { nodes: [{id, type, config}], edges: [{from, to, condition?}] }. For a linear sequence, each node has one outgoing edge. For a condition branch, the condition node has two edges: one for true, one for false.

The execute-workflow Edge Function is the engine. It starts at the entry node and processes nodes breadth-first. For each node it runs the action defined by the node's type: fetch() for HTTP requests, supabase.from().select() for DB queries, and conditional logic for branch nodes. Each step's output is stored in a run_context object that subsequent steps can reference using {{step_id.output.field}} syntax.

Webhook triggers expose a public URL (the trigger-webhook Edge Function URL) that external services can call. The trigger function validates an optional HMAC signature, creates a workflow_runs row, and calls execute-workflow. Cron triggers use pg_cron to call the trigger function on a schedule.

The run history viewer shows each workflow_runs row with its step_traces JSONB expanded in an Accordion. Each step trace shows the input, output, and status (completed, failed, skipped).

Final result

A visual workflow automation engine with webhook and cron triggers, multi-step execution, condition branching, and full run history.

Tech stack

LovableWorkflow builder and run history UI
SupabaseWorkflow definitions and run history storage
Supabase Edge FunctionsWorkflow execution engine and webhook receiver
pg_cronScheduled workflow triggers
shadcn/uiCard, Tabs, Badge, Select, Textarea, Accordion
React FlowVisual DAG rendering (optional enhancement)

Prerequisites

  • Lovable Pro account for multi-function Edge Function generation
  • Supabase project with SUPABASE_SERVICE_ROLE_KEY in Cloud tab → Secrets
  • pg_cron enabled in Supabase Dashboard → Database → Extensions
  • Understanding of directed acyclic graphs (a flowchart with no loops is a DAG)
  • Familiarity with async JavaScript — the execution engine uses Promise chains

Build steps

1

Create the workflow schema

Prompt Lovable to create all tables for workflow definitions, run history, and triggers. The schema supports the execution engine and the UI builder.

prompt.txt
1Create a workflow automation schema in Supabase.
2
3Tables:
4
51. workflows:
6 id uuid primary key default gen_random_uuid()
7 name text not null
8 description text
9 status text default 'draft' 'draft', 'active', 'paused', 'archived'
10 steps jsonb not null default '{"nodes": [], "edges": []}'
11 trigger_config jsonb default '{}' { type: 'webhook'|'cron'|'manual', cron_expression?: string, webhook_secret?: string }
12 created_by uuid references auth.users(id)
13 last_run_at timestamptz
14 run_count integer default 0
15 created_at timestamptz default now()
16 updated_at timestamptz default now()
17
182. workflow_runs:
19 id uuid primary key default gen_random_uuid()
20 workflow_id uuid references workflows(id) on delete cascade
21 status text default 'running' 'running', 'completed', 'failed', 'cancelled'
22 trigger_type text 'webhook', 'cron', 'manual'
23 trigger_payload jsonb
24 step_traces jsonb default '[]' array of { node_id, status, started_at, finished_at, input, output, error }
25 error_message text
26 started_at timestamptz default now()
27 finished_at timestamptz
28 duration_ms integer
29
303. workflow_triggers:
31 id uuid primary key default gen_random_uuid()
32 workflow_id uuid references workflows(id) on delete cascade
33 trigger_type text not null
34 cron_job_name text
35 webhook_path text unique
36 is_active boolean default true
37 created_at timestamptz default now()
38
39RLS:
40- workflows: authenticated users read/write their own (created_by = auth.uid())
41- workflow_runs: authenticated users read runs of their own workflows
42- workflow_triggers: authenticated users read/write their own workflow triggers
43
44Indexes:
45- workflow_runs(workflow_id, started_at DESC)
46- workflow_runs(status)
47
48Step node types (stored in steps.nodes[].type):
49- 'http_request': { method, url, headers, body_template }
50- 'supabase_query': { table, operation, filters, limit }
51- 'condition': { expression } evaluates a JS expression against run_context
52- 'delay': { duration_seconds }
53- 'send_email': { to_template, subject_template, body_template }
54- 'transform': { expression } transforms data between steps

Pro tip: Add a steps_version integer column to workflows. Increment it on every save. Store a snapshot of the steps JSONB in workflow_runs at the time of execution so old runs always show the exact steps that were run, not the current version.

Expected result: All three tables created with RLS. TypeScript types generated. The app shell shows in the preview.

2

Build the workflow execution engine Edge Function

Create the core execution engine that traverses the DAG and runs each step type. This is the most complex piece — take time to review the generated code carefully.

prompt.txt
1Create a Supabase Edge Function at supabase/functions/execute-workflow/index.ts.
2
3The function accepts POST with body: { workflow_id: string, run_id: string, trigger_payload: object }
4
5Execution engine logic:
6
71. Fetch the workflow by workflow_id. Get steps.nodes and steps.edges.
82. Fetch the workflow_runs row by run_id.
93. Build a run_context object: { trigger: trigger_payload, steps: {} }
10 - {{trigger.field}} resolves to trigger_payload.field
11 - {{steps.nodeId.output.field}} resolves to run_context.steps[nodeId].output.field
12
134. Find the entry node: the node that has no incoming edges.
14
155. Process nodes in order. For each node:
16 a. Log start: append to step_traces with status='running', started_at
17 b. Interpolate all config string values by replacing {{...}} placeholders with run_context values
18 c. Execute based on node.type:
19
20 'http_request': fetch(config.url, { method, headers, body: JSON.stringify(interpolated_body) })
21 - Output: { status, body: await response.json() }
22
23 'supabase_query': use service role client to call .from(config.table).select/insert/update based on operation
24 - Output: { rows, count }
25
26 'condition': evaluate a simple expression (e.g. '{{steps.step1.output.status}} === 200')
27 - Replace placeholders, then use a safe evaluator (not eval use a simple comparison parser)
28 - Output: { result: boolean }
29 - Next step: follow the edge where condition matches the result
30
31 'delay': await new Promise(resolve => setTimeout(resolve, config.duration_seconds * 1000))
32 - Note: max Edge Function timeout is 150s on paid Supabase. For longer delays, schedule a new run.
33 - Output: { delayed_ms }
34
35 'send_email': call the send-email Edge Function via fetch (if it exists) or direct Resend API
36 - Output: { message_id }
37
38 d. Store output in run_context.steps[node.id] = { output }
39 e. Update step_traces with status='completed', finished_at, output
40 f. On error: update step_trace with status='failed', error message. Set run status='failed'.
41
426. Find the next node by looking up edges where from = current_node_id
43 - For condition nodes: filter edges by edge.condition matching the output.result
44 - For all other nodes: take the single outgoing edge
45
467. When no more nodes to process: update workflow_runs.status='completed', finished_at, duration_ms
478. Update workflows.last_run_at and increment run_count

Pro tip: Persist the step_traces array after each step completes (not just at the end) using a partial Supabase update. This allows the run history UI to show real-time progress for long-running workflows.

Expected result: The execution engine Edge Function deploys. Calling it with a workflow_id and run_id processes each node in order, storing traces in workflow_runs. Simple HTTP and Supabase query steps execute correctly.

3

Build the webhook trigger endpoint

Create the webhook receiver Edge Function that external services call to trigger a workflow. It validates the request, creates a run, and invokes the execution engine.

supabase/functions/trigger-webhook/index.ts
1// supabase/functions/trigger-webhook/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
4
5const corsHeaders = {
6 'Access-Control-Allow-Origin': '*',
7 'Access-Control-Allow-Headers': 'authorization, x-client-info, content-type, x-webhook-signature',
8}
9
10serve(async (req: Request) => {
11 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
12
13 const url = new URL(req.url)
14 const workflowPath = url.searchParams.get('path')
15
16 const supabase = createClient(
17 Deno.env.get('SUPABASE_URL') ?? '',
18 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
19 )
20
21 const { data: trigger } = await supabase
22 .from('workflow_triggers')
23 .select('workflow_id, workflow:workflows(id, status, trigger_config)')
24 .eq('webhook_path', workflowPath)
25 .eq('is_active', true)
26 .single()
27
28 if (!trigger?.workflow_id) {
29 return new Response(JSON.stringify({ error: 'Webhook not found' }), { status: 404, headers: corsHeaders })
30 }
31
32 const workflow = trigger.workflow as { id: string; status: string; trigger_config: { webhook_secret?: string } }
33
34 if (workflow.status !== 'active') {
35 return new Response(JSON.stringify({ error: 'Workflow is not active' }), { status: 400, headers: corsHeaders })
36 }
37
38 if (workflow.trigger_config?.webhook_secret) {
39 const signature = req.headers.get('x-webhook-signature') ?? ''
40 const body = await req.text()
41 const encoder = new TextEncoder()
42 const keyData = encoder.encode(workflow.trigger_config.webhook_secret)
43 const key = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])
44 const sigBytes = new Uint8Array(signature.match(/.{2}/g)!.map((b) => parseInt(b, 16)))
45 const valid = await crypto.subtle.verify('HMAC', key, sigBytes, encoder.encode(body))
46 if (!valid) return new Response(JSON.stringify({ error: 'Invalid signature' }), { status: 401, headers: corsHeaders })
47 }
48
49 const payload = req.method === 'POST' ? await req.json().catch(() => ({})) : {}
50
51 const { data: run } = await supabase.from('workflow_runs').insert({
52 workflow_id: workflow.id,
53 trigger_type: 'webhook',
54 trigger_payload: payload,
55 status: 'running',
56 }).select().single()
57
58 if (!run) return new Response(JSON.stringify({ error: 'Failed to create run' }), { status: 500, headers: corsHeaders })
59
60 await fetch(`${Deno.env.get('SUPABASE_URL')}/functions/v1/execute-workflow`, {
61 method: 'POST',
62 headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')}` },
63 body: JSON.stringify({ workflow_id: workflow.id, run_id: run.id, trigger_payload: payload }),
64 })
65
66 return new Response(JSON.stringify({ run_id: run.id }), { headers: corsHeaders })
67})

Pro tip: Return the run_id in the webhook response immediately, before the execution Engine finishes. The caller can then poll GET /workflow-runs/:run_id to check the status. This makes the webhook endpoint non-blocking.

Expected result: The webhook Edge Function is accessible at a public URL. Sending a POST request to it creates a workflow_runs row and triggers execution of the linked workflow.

4

Build the workflow builder UI

Create the workflow builder page where users define steps and their connections. Steps are shown as stacked Cards with add/remove/reorder controls. The builder writes to the steps JSONB.

prompt.txt
1Build a workflow builder page at src/pages/WorkflowBuilder.tsx.
2
3Page layout:
4- Top bar: workflow name Input, status Badge, Save Button, Test Run Button
5- Main area: ordered list of step Cards
6
7Each step Card shows:
8- Step number and type Badge (HTTP, Query, Condition, Delay, Email)
9- Step name Input
10- Type-specific config fields:
11 - HTTP: URL Input, method Select (GET/POST/PUT/DELETE), Headers Textarea (JSON), Body Textarea
12 - Query: table Input, operation Select (select/insert/update/delete), filters Textarea
13 - Condition: expression Textarea with helper text showing available {{variables}}
14 - Delay: duration_seconds Input with helper 'Max 120s in Edge Functions'
15 - Email: to Input, subject Input, body Textarea
16- Between cards: a connector line with an 'Add Step' Button (adds after this step)
17- Remove step Button (trash icon) in the card header
18- Steps are reorderable with up/down arrow buttons
19
20Trigger configuration section (above steps):
21- Trigger type Select: Manual, Webhook, Cron
22- Webhook trigger shows the public webhook URL (trigger-webhook Edge Function URL + ?path=webhook_path)
23- Cron trigger shows a cron expression Input with a human-readable preview
24
25When Save is clicked:
26- Validate all required fields
27- Build the steps JSONB: nodes array with sequential edges (each step connects to the next)
28- Save to workflows table via Supabase update
29
30Test Run Button:
31- Opens a Dialog with a JSON editor for test trigger_payload
32- On confirm: creates a workflow_runs row and calls execute-workflow
33- Navigates to /workflows/:id/runs/:run_id after submission

Pro tip: Show the {{steps.stepId.output}} variables available to each step based on the steps that come before it in the sequence. As the user builds the workflow, update a tooltip on each config Textarea showing which variables are available at that point.

Expected result: The workflow builder shows step cards in sequence. Adding and removing steps updates the steps JSONB. Saving persists to the database. Test Run creates a run and navigates to the run viewer.

5

Build the run history viewer

Create the run history page showing all past executions with step-by-step trace expansion. This is the debugging interface for when workflows fail.

prompt.txt
1Build a run history viewer.
2
31. src/pages/WorkflowRuns.tsx run list:
4 - DataTable with columns: Run ID (first 8 chars), Status Badge (green=completed, red=failed, blue=running), Trigger Type, Started At, Duration
5 - Filter by status using a Select above the table
6 - Clicking a row navigates to /workflows/:id/runs/:run_id
7 - Auto-refresh running runs every 3 seconds using useEffect with a setInterval
8
92. src/pages/WorkflowRunDetail.tsx run detail:
10 - Header: status Badge, trigger type, started/finished timestamps, total duration
11 - Trigger Payload Card: shows the JSON that triggered the run in a code block
12 - Steps Accordion (shadcn/ui Accordion):
13 - One AccordionItem per step trace
14 - Trigger: step name + type Badge + status icon (checkmark, X, clock)
15 - Content: two columns Input JSON and Output JSON in monospace code blocks
16 - Failed steps show the error message in a destructive Alert
17 - 'Re-run Workflow' Button: creates a new run with the same trigger payload
18 - For running workflows: show a progress indicator and auto-expand the currently executing step

Pro tip: Add a diff view between the workflow's current steps and the steps_snapshot stored in the run. If the workflow has been modified since the run, show a banner: 'This run used an older version of the workflow steps.'

Expected result: The run list shows all past runs with status and duration. Clicking a run shows the step-by-step trace with input/output JSON. Failed steps show the error. Running runs auto-refresh.

Complete code

supabase/functions/trigger-webhook/index.ts
1import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
2import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
3
4const corsHeaders = {
5 'Access-Control-Allow-Origin': '*',
6 'Access-Control-Allow-Headers': 'authorization, x-client-info, content-type, x-webhook-signature',
7 'Content-Type': 'application/json',
8}
9
10serve(async (req: Request) => {
11 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
12
13 const url = new URL(req.url)
14 const webhookPath = url.searchParams.get('path')
15
16 if (!webhookPath) {
17 return new Response(JSON.stringify({ error: 'Missing path parameter' }), { status: 400, headers: corsHeaders })
18 }
19
20 const supabase = createClient(
21 Deno.env.get('SUPABASE_URL') ?? '',
22 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
23 )
24
25 const { data: trigger } = await supabase
26 .from('workflow_triggers')
27 .select('workflow_id')
28 .eq('webhook_path', webhookPath)
29 .eq('is_active', true)
30 .single()
31
32 if (!trigger) {
33 return new Response(JSON.stringify({ error: 'Webhook not found' }), { status: 404, headers: corsHeaders })
34 }
35
36 const { data: workflow } = await supabase
37 .from('workflows')
38 .select('id, status, trigger_config')
39 .eq('id', trigger.workflow_id)
40 .single()
41
42 if (!workflow || workflow.status !== 'active') {
43 return new Response(JSON.stringify({ error: 'Workflow not active' }), { status: 400, headers: corsHeaders })
44 }
45
46 const bodyText = await req.text()
47 let payload: Record<string, unknown> = {}
48 try { payload = JSON.parse(bodyText) } catch { /* non-JSON body */ }
49
50 const { data: run, error: runError } = await supabase
51 .from('workflow_runs')
52 .insert({
53 workflow_id: workflow.id,
54 trigger_type: 'webhook',
55 trigger_payload: payload,
56 status: 'running',
57 started_at: new Date().toISOString(),
58 })
59 .select('id')
60 .single()
61
62 if (runError || !run) {
63 return new Response(JSON.stringify({ error: 'Failed to create run' }), { status: 500, headers: corsHeaders })
64 }
65
66 // Fire and forget — execute in background
67 fetch(`${Deno.env.get('SUPABASE_URL')}/functions/v1/execute-workflow`, {
68 method: 'POST',
69 headers: {
70 'Content-Type': 'application/json',
71 Authorization: `Bearer ${Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')}`,
72 },
73 body: JSON.stringify({ workflow_id: workflow.id, run_id: run.id, trigger_payload: payload }),
74 }).catch(console.error)
75
76 return new Response(JSON.stringify({ run_id: run.id, status: 'triggered' }), { headers: corsHeaders })
77})

Customization ideas

Visual DAG editor with React Flow

Replace the linear step list with a React Flow canvas where nodes are draggable and edges can be drawn between them. Store the node positions in the steps JSONB alongside the execution config.

Loop step type

Add a 'loop' step type that iterates over an array from a previous step's output. The execution engine spawns child run traces for each iteration and merges results back into the main context.

Sub-workflow step

Add a 'workflow' step type that triggers another workflow by ID and waits for it to complete. This enables modular workflow composition — shared utility workflows called from multiple parent workflows.

Approval gate step

Add a 'wait_for_approval' step type. The engine pauses the run and sends an email with approve/deny links. Clicking the link calls an Edge Function that resumes or cancels the run.

Workflow templates

Add a template library page showing pre-built workflows (e.g. 'New user onboarding', 'Failed payment retry'). Users clone a template to their workflows table and customize it.

Run analytics dashboard

Add a workflows analytics page showing success rate, average duration, most common failure points, and runs per day for all workflows. Feed it from aggregated queries on workflow_runs and step_traces.

Common pitfalls

Pitfall: Using eval() to evaluate condition expressions in the execution engine

How to avoid: Implement a safe expression evaluator that only supports comparisons and logical operators. Replace {{placeholders}} with their resolved values, then use a simple parser that handles ===, !==, >, <, &&, || without eval.

Pitfall: Making the webhook trigger synchronous — waiting for workflow execution before responding

How to avoid: Create the workflow_runs row and return the run_id immediately. Fire the execute-workflow Edge Function call using fetch without await (fire-and-forget). The caller can poll run status separately.

Pitfall: Not storing a steps snapshot in workflow_runs at the time of execution

How to avoid: Copy the workflow's current steps JSONB into a steps_snapshot column in workflow_runs when the run starts. The run detail viewer uses steps_snapshot, not the live workflow steps.

Pitfall: Building infinite loops by connecting a step's output edge back to an earlier node

How to avoid: Validate the DAG before saving by checking for cycles. Use a depth-first search: if you encounter a node you've already visited in the current path, the graph has a cycle. Reject the save with a validation error.

Pitfall: Storing sensitive data (API keys, passwords) in workflow step configurations

How to avoid: Use secret references in workflow configs instead of values: {{secret.MY_API_KEY}}. In the execution engine, resolve secret references using Deno.env.get() at runtime. Document which secrets need to be set in Cloud tab → Secrets.

Best practices

  • Store a steps_snapshot in workflow_runs at execution time so run history always reflects what actually ran
  • Validate the DAG for cycles before saving — prevent infinite loops at the UI layer before they reach the execution engine
  • Never use eval() for condition expressions — implement a restricted expression parser
  • Make webhook endpoints non-blocking — create the run row and return immediately, execute in background
  • Add a maximum execution timeout at the engine level (separate from Edge Function timeout) and mark runs as 'timeout' after N seconds
  • Log the full input and output of each step in step_traces — this is the primary debugging tool when workflows fail
  • Use idempotency keys on webhook triggers to prevent duplicate runs from retry logic in calling services
  • Cap the maximum number of nodes in a workflow (e.g. 50) to prevent pathologically complex workflows that exhaust Edge Function resources

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a workflow execution engine in a Supabase Edge Function (Deno runtime). The engine processes a DAG of steps defined as { nodes: [{id, type, config}], edges: [{from, to, condition?}] }. I need to evaluate condition expressions like '{{steps.step1.output.status}} === 200' safely without using eval(). The expression needs to support ===, !==, >, <, >=, <=, &&, and ||. How do I implement a safe expression evaluator in TypeScript that handles these operators after resolving {{placeholder}} values?

Lovable Prompt

Add a loop step type to my workflow automation engine. The loop step config has: source_path (a {{placeholder}} that resolves to an array), item_variable (the name to use for each item, e.g. 'item'), and a sub_steps array of step configs. The execution engine should iterate over the array, run the sub_steps for each item (with {{item.field}} available as context), collect the outputs into a results array, and store them as the loop step's output. Show how to add this to the existing execution engine.

Build Prompt

In my Lovable workflow builder, I store the workflow steps as a JSONB DAG in Supabase. The frontend shows steps as a linear list of Cards. I want to validate that the edges array creates a valid DAG (no cycles) before saving. Where should this validation run — in a Supabase CHECK constraint using a custom PostgreSQL function, in a Supabase Edge Function, or in the React frontend? What's the recommended approach for validating graph structure in a Lovable app?

Frequently asked questions

What's the maximum workflow execution time?

Supabase Edge Functions time out after 30 seconds on the free tier and 150 seconds on paid plans. For workflows with delay steps or many HTTP calls, you may hit this limit. The workaround is to split long workflows into sub-workflows: the first workflow creates a scheduled workflow_runs entry, and a pg_cron job resumes it later.

Can workflows call other workflows?

Yes, by adding a 'workflow' step type whose execution fetches another workflow's definition and calls execute-workflow recursively (or in a separate Edge Function invocation). To prevent infinite recursion, add a max_depth parameter to execute-workflow that fails if the current depth exceeds your limit (recommended: 5).

How do I handle workflow failures and retries?

The execution engine sets workflow_runs.status = 'failed' and stores the error in the failing step's trace. Add a max_retries field to workflows and a retry_count to workflow_runs. When a run fails, a pg_cron job checks for failed runs with retry_count < max_retries and re-creates a new run with the same trigger payload.

Can I test a workflow before activating it?

Yes. The Test Run button on the workflow builder creates a run with trigger_type = 'manual' and a custom test payload. The execution engine runs the full workflow using the test payload. The run history shows the results. Workflows in 'draft' status can be manually triggered but not triggered by webhooks or cron.

How do I pass data from one step to the next?

The run_context object accumulates each step's output keyed by node ID. In subsequent step configs, use {{steps.node_id.output.field}} placeholders. The execution engine resolves these before running each step. For example, if step 'fetch-user' returns { output: { email: 'user@example.com' } }, the next step can use {{steps.fetch-user.output.email}}.

What's the difference between a workflow trigger and a cron job?

A workflow trigger is the mechanism that starts a workflow. Webhook triggers start workflows when an external service calls the trigger URL. Cron triggers start workflows on a schedule. Both create a workflow_runs row and call execute-workflow. The trigger type is stored in workflow_runs.trigger_type for auditing.

Can multiple instances of the same workflow run simultaneously?

Yes by default. If a cron trigger fires while a previous run is still executing, both run concurrently. To prevent this, add a UNIQUE partial index on workflow_runs(workflow_id) WHERE status = 'running'. Attempting to create a second run while one is running will fail — you can catch this and skip or queue the new run.

How do I monitor workflow health across all workflows?

Build a dashboard page that queries workflow_runs grouped by workflow_id, showing success rate, average duration, and last run status for each workflow. Add a global alert if any workflow has a failure rate above 10% in the last 24 hours. Query: SELECT workflow_id, COUNT(*) FILTER (WHERE status='failed') / COUNT(*)::float as failure_rate FROM workflow_runs WHERE started_at > now() - interval '1 day' GROUP BY workflow_id.

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.