VibeWeek
Home/Grow/In-Product Workflow & Automation Builder — Chat Prompts

In-Product Workflow & Automation Builder — Chat Prompts

⬅️ Back to 6. Grow

If your B2B SaaS hits a moment around Series A where customers ask "can you make X happen automatically when Y?" — that's the signal you should consider building an in-product workflow / automation builder. Linear's Cycle automations, Notion automations, Airtable automations, HubSpot workflows, Salesforce Process Builder — every modern B2B SaaS eventually ships some version of this. The naive shape: "we'll just integrate with Zapier; that solves it." It's a starting point but: Zapier integrations require customers to build/maintain Zaps in another tool, with a separate bill, separate auth, separate debugging surface. For deep workflows, customers want it INSIDE your product, with full type safety, no separate billing, instant runs, integrated history, and triggers/actions that know about your domain primitives.

Get this right and customers automate ~30-50% of their repetitive operations, expansion revenue compounds (more workflows = more sticky), and you reduce support volume on "can it do X?" questions. Get it wrong and you ship a broken Zapier copy that nobody trusts, customers build 200 broken workflows that fire at 3am, your support team handles "why did this run / not run?" tickets every day, and you have an undeprecateable feature.

This chat walks through implementing a real workflow automation engine: triggers, conditions, actions, the visual builder UI, the execution runtime, debugging tools, error handling, quotas, and the operational realities of running customer-defined workflows at scale.

What you're building

  • A workflow data model (trigger + conditions + actions; versioned)
  • A trigger system (event-driven; based on your existing event bus)
  • A conditions / filtering engine (when does the workflow apply?)
  • An actions library (what can the workflow do?)
  • A visual workflow builder (the customer-facing UI)
  • An execution runtime (durable; idempotent; observable)
  • An error / retry / pause system
  • A debugging UI (run history; step-through; replay)
  • Quotas + abuse controls
  • Customer-facing analytics (how often each workflow ran)

1. Decide the scope BEFORE designing

Help me decide what shape of automation builder I actually need.

Three architectural shapes — pick consciously:

LEVEL 1: PRESET WORKFLOWS (templates only)
- You ship 10-30 pre-built automations
- Customer enables/disables; can't edit logic
- Configurable parameters but not the steps
- Examples: "When task is overdue → send email"; "When document is shared → notify slack"
- Pros: fast to ship; bounded scope; customer can't break things
- Cons: not flexible; misses long-tail cases
- Time to ship: 4-8 weeks

LEVEL 2: SIMPLE BUILDER (linear: trigger → conditions → 1-3 actions)
- Customer picks trigger from dropdown
- Adds optional conditions
- Picks 1-3 actions
- No branches; no loops; no nested conditions
- Examples: HubSpot workflows v1; Notion automations; Linear's "When status changes"
- Pros: powerful enough for 80% of customer needs; manageable complexity
- Cons: complex flows require multiple workflows
- Time to ship: 12-20 weeks

LEVEL 3: FULL BUILDER (branches, loops, parallel paths, sub-workflows)
- DAG-shaped workflows
- Conditional branches
- Loops + parallel execution
- Sub-workflows / reusable building blocks
- Variables passed between steps
- Examples: HubSpot enterprise workflows; Salesforce Flow; n8n
- Pros: customers can build anything; sticks like cement
- Cons: months of engineering; debugging hell; support load
- Time to ship: 24-52 weeks

LEVEL 4: NO-CODE PROGRAM (with code blocks)
- Full builder + JS/Python code blocks
- Custom HTTP requests, JSON parsing, etc.
- Examples: n8n, Make.com, Zapier code steps
- Pros: ultimate flexibility
- Cons: customer-side code execution liability; security audits; 9+ months engineering
- Time to ship: 1+ year

DEFAULT FOR MOST B2B SaaS:
- v1: Level 1 (preset workflows) — ship in a quarter; learn what customers want
- v2: Level 2 (simple builder) — based on what you learned from v1 usage
- v3: Level 3 — only if customer demand justifies (most never need this)

Which to ship FIRST:
- Have <20 customers asking for it: ship Level 1 templates
- Have 20-100 customers asking + clear common pattern: ship Level 2
- Don't pre-build Level 3 without specific buyer; you'll regret it

Output: a clear scope decision; ship plan; explicit "we are NOT shipping" boundary.

Output: a scope statement that prevents over-building.

2. Design the data model

Now design the workflow data model for [Postgres / Drizzle / your stack].

Core schema:

workflows (
  id              uuid pk
  workspace_id    uuid not null  -- multi-tenancy boundary
  name            text not null
  description     text
  status          text  -- 'draft' | 'active' | 'paused' | 'archived'
  trigger_type    text not null  -- e.g. 'task_created', 'document_updated', 'comment_added'
  trigger_config  jsonb  -- parameters specific to trigger type
  conditions      jsonb  -- array of condition objects (AND/OR groups)
  actions         jsonb  -- array of action objects, ordered
  version         int    -- bumped on each save
  created_by      uuid
  created_at      timestamptz
  updated_at      timestamptz
  last_run_at     timestamptz  -- denormalized for UI
  total_runs      bigint default 0  -- denormalized counter
  failed_runs     bigint default 0
)

workflow_runs (
  id              uuid pk default gen_uuid_v7()  -- sortable by time
  workflow_id     uuid not null
  workflow_version int not null  -- which version of the workflow ran
  workspace_id    uuid not null
  trigger_event_id uuid  -- which event triggered this
  trigger_payload jsonb  -- snapshot of trigger data
  status          text  -- 'pending' | 'running' | 'completed' | 'failed' | 'skipped'
  started_at      timestamptz
  completed_at    timestamptz
  error_message   text
  step_results    jsonb  -- per-step outcome
  retry_count     int default 0
  parent_run_id   uuid  -- for retries; points to original
)

workflow_step_results (  -- normalized version of step_results above; pick one
  run_id          uuid
  step_index      int
  step_type       text  -- 'condition' | 'action'
  step_action_type text -- e.g. 'send_email', 'create_task', 'webhook'
  status          text
  input           jsonb
  output          jsonb
  duration_ms     int
  error_message   text
  PRIMARY KEY (run_id, step_index)
)

workflow_versions (
  id              uuid pk
  workflow_id     uuid not null
  version         int not null
  trigger_type    text
  trigger_config  jsonb
  conditions      jsonb
  actions         jsonb
  saved_by        uuid
  saved_at        timestamptz
  UNIQUE (workflow_id, version)
)

Indexes:
- (workspace_id, status='active') for triggering workflow lookups
- (workflow_id, started_at DESC) for run history UI
- (workspace_id, started_at DESC) for workspace-wide debugging UI

Why version workflows:
- A run started under v3; user edits to v4 mid-run → run completes with v3 logic
- Customer can roll back: "use v2 again"
- Debugging: "this run was from v3 with this exact config"

Why store actions as jsonb (not normalized rows):
- Schema evolves with new action types
- Per-action config varies wildly (email-action has 'recipient'; webhook-action has 'url')
- JSONB indexes available if you need to query specific keys

Implement:
1. The migration SQL
2. The Drizzle/Prisma model
3. RLS policies (Supabase)
4. The version-on-save trigger
5. TypeScript types for trigger + condition + action shapes
6. Validation: a workflow can be saved only if its trigger_config / conditions / actions all validate against their respective schemas

Output: a schema that supports versioning + audit + replay.

3. Design the trigger system

Now build the trigger system.

Triggers are how workflows start. They fire on events you already emit elsewhere in your product (see [Activity Feed & Timeline](./activity-feed-timeline-implementation-chat) for the events backbone).

Architecture:
1. Your product emits events (e.g. emitEvent({ verb: 'task_created', ...}))
2. Trigger router subscribes to events
3. Router queries: "are there any workflows in this workspace where trigger_type matches this event?"
4. For each matching workflow, evaluate trigger_config (does this event meet the trigger criteria?)
5. If yes, queue a workflow run
6. Workflow execution worker picks up the run

Trigger types to ship:

Built-in entity triggers:
- entity_created (e.g. task_created, document_created)
- entity_updated
- entity_field_changed (specific field)
- entity_deleted
- entity_status_changed (specific status transitions)

User triggers:
- user_added_to_workspace
- user_role_changed
- comment_added
- mention_created

Time triggers:
- scheduled (cron-like; "every Monday at 9am")
- delayed_after_event (e.g. "30 days after document_created with no edits")

External triggers:
- webhook_received (from a third party)
- email_received (if you have a parsing inbox)

Custom triggers:
- API call from customer's code (POST /api/workflows/:id/trigger)

Trigger config schema (per type):
- entity_created: { entity_type: 'task', filter: {project_id?, assignee_id?, ...} }
- entity_field_changed: { entity_type: 'task', field: 'status', from?: 'todo', to?: 'done' }
- scheduled: { cron: '0 9 * * 1', timezone: 'America/Los_Angeles' }

Architecture for triggering:

When event emitted → publish to internal queue 'workflow_triggers' (Pub/Sub or similar)
Trigger Router (worker):
  loop:
    msg = queue.pull()
    workflows = db.query("SELECT * FROM workflows WHERE workspace_id = $1 AND status='active' AND trigger_type=$2", msg.workspace_id, msg.event_type)
    for w in workflows:
      if matches_trigger_config(w.trigger_config, msg.payload):
        create workflow_runs row (status='pending')
        publish to 'workflow_executions' queue

Performance considerations:
- Indexed lookup by (workspace_id, status, trigger_type)
- Cache hot-workspace workflow lookups (60s TTL)
- For high-volume events (e.g., page_view), don't allow them as workflow triggers (DoS risk)

Trigger config evaluation:
- JSON-schema-validate at workflow save time
- Match at trigger time using JSONLogic or your own eval

Anti-patterns:
- Don't allow direct DB triggers (Postgres triggers); workflow logic lives in app
- Don't fire workflows synchronously in the request path (always async)
- Don't allow customer to subscribe to events that contain other tenants' data

Implement:
1. The trigger router
2. The matcher (trigger_config evaluation)
3. The schema for each trigger type
4. The performance optimizations (caching, indexed lookups)
5. The customer-facing trigger picker UI (more in step 6)

Output: triggers that respond to events without DDoS-ing the DB.

4. Design conditions + actions

Now build the conditions and actions library.

CONDITIONS: filtering after trigger fires

Examples:
- entity field equals X
- entity assignee is in [list]
- date is more than N days from now
- count of related entities > N
- field changed from X to Y (inferred from snapshot)

Condition schema:

type Condition =
  | { type: 'and'; children: Condition[] }
  | { type: 'or'; children: Condition[] }
  | { type: 'field_equals'; field: string; value: any }
  | { type: 'field_in'; field: string; values: any[] }
  | { type: 'field_changed'; field: string }
  | { type: 'field_not_changed'; field: string }
  | { type: 'date_more_than'; field: string; value: number; unit: 'minutes' | 'hours' | 'days' }
  | { type: 'count_greater'; relation: string; value: number }
  // ... more types

Evaluation:
- conditions evaluated against trigger payload + entity snapshot
- can fetch additional context if needed (related entities)
- short-circuit (AND stops at first false; OR stops at first true)

ACTIONS: what the workflow does

Examples:
- send_email
- create_task
- update_field
- add_comment
- assign_to_user
- send_notification
- send_webhook
- run_other_workflow  (sub-workflows; advanced)

Action schema:

type Action =
  | { type: 'send_email'; to: TemplateString; subject: TemplateString; body: TemplateString }
  | { type: 'create_task'; project_id: string; title: TemplateString; assignee?: string }
  | { type: 'update_field'; entity_id: string; field: string; value: TemplateString }
  | { type: 'add_comment'; entity_id: string; body: TemplateString }
  | { type: 'send_webhook'; url: string; method: 'POST'; body: jsonbTemplate; headers?: ... }
  | { type: 'send_notification'; user_id: string; message: TemplateString }
  | { type: 'wait'; duration: number; unit: 'minutes' | 'hours' | 'days' }
  // ... more types

TemplateString supports references to trigger payload + previous step outputs:
- "{{trigger.entity.title}}"
- "{{trigger.actor.email}}"
- "{{step1.output.id}}"

Implementation:
- Use safe template engine (NOT eval; sandboxed; e.g. Handlebars with escape)
- Validate template at save time (fields exist on the schema)
- Render template at execution time

Action handlers:
- Each action type has a handler function
- Handler signature: async (input: ActionInput, context: ExecutionContext) => ActionResult
- Handler is responsible for: validation, execution, error handling, idempotency

Idempotency:
- Each action run has a unique run_id + step_index
- Dedupe by (workflow_run_id, step_index) — never run the same step twice
- For external actions (send_email, webhook): include idempotency key in API call

Error handling per action:
- Each action can mark itself: 'fatal' (whole workflow fails) or 'non-fatal' (continue to next)
- Customer can configure: "if action fails, [skip / fail workflow / retry]"
- Default: retry transient errors 3x with exponential backoff, then mark step failed

Implement:
1. The condition schema + evaluator
2. The action schema + handler interface
3. The template engine
4. Per-action handlers (start with 5-10 most common)
5. The validation layer (workflow can't save with invalid action config)
6. The execution context (passes data between steps)

Output: a flexible-but-bounded set of actions that compose.

5. Build the execution runtime

Now the execution runtime — where workflows actually run.

Architecture:

Workflow execution is a durable async job. Use [Inngest / Trigger.dev / Temporal / your queue + worker] for orchestration. DO NOT run workflows synchronously in HTTP request handlers.

Execution flow:

1. Trigger router enqueues workflow_run (status='pending')
2. Worker picks up the run; status='running'
3. For each step (in order):
   a. Evaluate conditions (skip workflow if any fail)
   b. Render action template with context
   c. Execute action via handler
   d. Capture step result
   e. Update workflow_run with progress
4. Mark workflow_run completed (or failed)
5. Update workflow.last_run_at, workflow.total_runs

Idempotency:
- Step-level idempotency keys
- Worker can be killed mid-run; on restart, pick up where it left off
- Use [Inngest's step API / Trigger.dev's step API / your durable workflow lib]

Concurrency:
- Per-workflow concurrency limit (don't run 1000 instances of same workflow simultaneously)
- Per-workspace concurrency limit (DoS protection)
- Rate limit on external action calls (don't exhaust customer's email quota)

Timeouts:
- Per-step timeout (default 30s; configurable per action type)
- Whole-workflow timeout (default 5 min)
- Long-running workflows: use 'wait' action explicitly

Retries:
- Transient errors (network, 5xx): retry 3x with exponential backoff
- Permanent errors (4xx, validation): no retry; mark failed
- Customer-configurable retry policy per workflow

Pausing / waiting:
- 'wait' action puts workflow in 'paused' state with resume_at timestamp
- Cron job picks up paused workflows when resume_at <= now
- Use durable workflow runtime for this (Temporal, Inngest)

Step results capture:
- Every step's input + output captured to workflow_step_results
- Useful for debugging
- TTL: 30-90 days for completed runs (don't store forever; storage explosion)

Failure modes:
- Step fails: log; mark failed; alert customer (notification + email)
- Workflow exceeds timeout: kill; mark failed; alert
- Worker crashes mid-step: durable runtime resumes from last checkpoint
- Database unreachable: retry transparently; mark failed only after permanent

Implement:
1. The worker / execution loop
2. The step-level idempotency
3. The retry logic
4. The timeout enforcement
5. The pause/resume for 'wait' actions
6. The metrics emission (per workflow, per action, per workspace)

Output: a runtime that survives crashes and customer-induced chaos.

6. Build the visual workflow builder UI

Now build the customer-facing UI.

Key UX decisions:

A. Linear vs DAG (Level 2 vs Level 3)
- Linear: simple list of trigger → conditions → actions; ship this first
- DAG: visual flowchart with branches; advanced; defer

B. Real-time validation
- As user picks trigger: show available fields
- As user adds conditions: show available operators per field type
- As user adds actions: show template fields available
- All validated server-side on save

C. Test-run capability
- "Test this workflow" button
- Customer picks a sample trigger event (or system shows recent events to pick from)
- Workflow runs in dry-run mode (no side effects)
- Show step-by-step results

UI components:

Builder page (/automations/[id]/edit):

Header:
- Workflow name (editable inline)
- Status toggle (draft / active / paused)
- "Test" button
- "Save" button (with version bump)
- "Run history" link

Body:
1. Trigger section
   - "When this happens" big card
   - Trigger type dropdown (Task created / Document updated / etc.)
   - Trigger config form (varies per type)

2. Conditions section
   - "If these conditions are met" (optional)
   - Add condition button
   - Each condition: field dropdown, operator dropdown, value input
   - AND/OR grouping

3. Actions section
   - "Then do this" big block
   - Each action: action type dropdown, action config form
   - Drag to reorder
   - Add action button

4. Footer
   - Test button (live)
   - Save button
   - Last edited info + version

Field references:
- When typing in template fields, autocomplete suggests {{trigger.fieldName}}
- Show available fields based on trigger type
- Validate references at save time

Action picker:
- Categorized: Communication / Tasks / Records / External / Wait
- Search box
- Per action: short description, parameters required

Best practices:
- Save early, save often (auto-save drafts every 30s)
- Validate before "Activate"
- Inline help for each trigger / condition / action type

Implement:
1. The builder page layout
2. The trigger picker + config form
3. The condition builder
4. The action picker + config form
5. The template autocomplete
6. The test-run UI
7. The save / activate flow
8. The version history view

Output: a builder customers actually finish setting up.

7. Build the run history + debugging UI

Customers WILL ask: "did this workflow run? why didn't it run? what did it do?"

Surface a great answer.

Run history page (/automations/[id]/runs):

List view:
- Last 100 runs (cursor pagination)
- Each row: timestamp, status (✓ success / ✗ failed / ⏸ paused), trigger summary, duration
- Filter: status, date range
- Search: by trigger payload field

Click a run → detail view:
- Trigger: what fired this; full payload (JSON viewer)
- Each step: 
  - Step #, action type, status, duration
  - Input + output (JSON)
  - Error message if failed
- Total duration
- Retry button (re-run this exact trigger)

Debugging panel:
- "Why didn't this run?" explainer
  - "Trigger fired at X" → "Conditions evaluated to false because [field] was [actual] not [expected]"
- Step-by-step replay (animation through the flow)
- Compare runs (run A vs B; show diffs)

Customer-facing analytics:
- Total runs (last 7/30/90 days)
- Success rate
- Average duration
- Top failure reasons

Implement:
1. The run list view
2. The run detail view (with JSON inspector)
3. The "why didn't it run" explainer logic
4. The retry button
5. The customer-facing analytics

Output: customers can self-debug; reduces support load 10x.

8. Quotas, abuse, and operational guardrails

Customer-defined workflows are a vector for misuse — accidental and malicious. Plan for it.

Quotas (per workspace, per plan tier):

Free tier:
- 5 active workflows
- 1,000 workflow runs / month
- No external HTTP actions (no send_webhook)

Standard tier:
- 25 active workflows
- 50,000 runs / month
- Webhooks to allowlisted domains only

Pro tier:
- 100 active workflows
- 500,000 runs / month
- Webhooks to any domain
- Code blocks (if Level 4)

Enterprise tier:
- Custom

Per-workflow guardrails:
- Concurrency limit: 50 parallel runs per workflow (prevents runaway loops)
- Steps per workflow: 20 max (prevents infinite chains)
- Wait duration max: 30 days (prevents zombie workflows)
- Loop detection: workflow A triggers workflow B which triggers A → block on detection

Abuse detection:
- Spike alert: workflow runs > 10x normal in 1 hour
- Failed-run spike: 90%+ failures in 1 hour → auto-pause + email
- Webhook to suspicious domain: rate limit + alert
- Email-bombing: workflow sends 100+ emails to same address in 1 hour → block

Customer-facing display:
- Quotas visible in settings: "23 / 25 active workflows" 
- Usage chart: runs per day vs limit
- Approaching limit: notify before hard block

Abuse-response runbook:
- Customer's workflow is making 100 webhooks/sec to external domain → auto-pause
- Customer's workflow is in infinite loop → auto-pause + email
- Customer-facing UI shows pause reason + "Reactivate" button

Internal admin tools:
- Force-pause any workflow
- Force-disable workflow runs for a workspace
- Force-purge run history for a workspace
- View execution metrics by workspace

Implement:
1. The quota enforcement (per-tier limits)
2. The concurrency limits per workflow
3. The loop detection
4. The abuse detection alerts
5. The auto-pause flows
6. The internal admin tools

Output: a system that survives customer creativity.

9. Edge cases + operational concerns

Walk me through edge cases:

1. Workflow runs while customer is editing it
   - Capture workflow_version at run start
   - Run completes with that version's logic
   - Customer's edits apply to NEXT run

2. Action handler fails halfway (e.g. emails 3 of 5 recipients)
   - Idempotency: each recipient is its own sub-step
   - On retry: skip already-sent (deduplicate by hash of recipient + run_id)

3. Customer deletes the entity that triggered a workflow
   - Run already in progress: continue with snapshot from trigger_payload
   - Action references entity that no longer exists: action fails gracefully
   - Don't crash workflow; mark step skipped/failed

4. Trigger event volume too high
   - e.g. customer sets workflow on "every page_view"; their site gets 1M views/day
   - Detect: triggers per workflow per minute > threshold
   - Action: block at trigger router; alert customer; require throttle config

5. Workflow A creates entity X; workflow B triggers on entity_created → recursion risk
   - Add metadata to workflow-created entities ("created_by_workflow_id")
   - Workflow B can opt-out: "skip if created by another workflow"
   - Hard limit: max 5 levels of workflow chaining per trigger event

6. Customer workspace deleted; their workflows hanging
   - Cascade-delete all workflows + runs
   - GDPR: ensure run history is purged on tenant delete

7. Workspace plan downgrade past quota
   - Customer was on Pro (100 workflows); downgrades to Standard (25 limit)
   - Don't auto-disable workflows
   - Show banner: "You have 73 workflows; 25 allowed on this plan. Disable some or upgrade."
   - Block ACTIVATION of new workflows

8. Email action used to spam
   - Each workflow's email actions go through email validation
   - Sender reputation tracking
   - Customer-bounded daily email cap (e.g. 1000/day; raise on request)

9. Customer wants to migrate workflow to another workspace
   - Export-import via JSON
   - Reassign user references (assignees might not exist in new workspace)
   - Test before activating

10. Workflow logic so complex it's effectively code
    - At some point, customers want loops, conditionals, code
    - Plan for graceful escalation: "this workflow is complex; consider using our API directly"
    - Or: ship Level 4 (code blocks) when demand justifies

11. Customer's webhook endpoint is down
    - Retry 3x with exponential backoff
    - After 3 failures: mark step failed; notify customer
    - After 100 failures in 24h: pause that workflow's webhook actions

12. Workflow that runs 1M times per day (legitimate, like syncing tickets)
    - Confirm pricing tier supports this volume
    - Optimize: batch executions, parallelize action handlers
    - Internal budget: this customer might be 30% of your workflow infra cost; price accordingly

For each, walk me through code change + customer comms.

Output: real-world operational maturity.

10. Recap

What you've built:

  • Workflow data model (versioned, audit-able)
  • Trigger system tied to your event bus
  • Condition + action library (extensible)
  • Visual workflow builder UI (linear v1; DAG later if needed)
  • Durable execution runtime (Inngest / Trigger.dev / Temporal-backed)
  • Run history + debugging UI
  • Quotas + abuse controls
  • Customer-facing analytics
  • Operational runbooks for misuse

What you're explicitly NOT shipping in v1:

  • Visual DAG builder (defer to Level 3)
  • Code blocks (defer to Level 4)
  • Sub-workflows / reusable building blocks (defer)
  • Cross-workspace workflows (anti-pattern; never)
  • Custom HTTP actions to arbitrary domains on Free tier (security)
  • AI-suggested workflows (defer; useful but not v1)
  • Cron-on-arbitrary-time-zones beyond top 30 (start with the obvious)

Ship Level 1 (templates) → see what customers want → Level 2 (simple builder) → measure → Level 3 only if explicitly needed.

The biggest mistake teams make: building Level 3 (full DAG with branches + loops) when 80% of customers needed Level 1 (templates with parameters). The complex builder costs 3-4x more to ship + maintain + debug.

The second mistake: not investing in run history + debugging UI. Customers will trust workflows only if they can SEE what they did. Without it, support tickets dominate.

The third mistake: treating workflow execution as synchronous. ALWAYS async via durable queue. Otherwise customer's bad workflow takes down your API.

See Also