Skip to content

Workflow Automation

!!! info "TL;DR" Workflows trigger automated actions when events occur. Powered by Inngest for durable execution. Built-in actions: send email, create GitHub issue, update status, Slack notify. Custom actions supported.

What is a Workflow?

A workflow is a set of automated actions that execute when a trigger condition is met. For example:

  • Trigger: User submits feedback with "bug" in the title
  • Action 1: Create a GitHub issue
  • Action 2: Send email to engineering team
  • Action 3: Update submission status to "Under Review"

Workflows are durable. If an action fails (e.g., GitHub is down), Inngest retries with exponential backoff. The workflow resumes from the failed step, not from the beginning.

Workflow Definition

A workflow consists of:

  1. Trigger — The event that starts the workflow
  2. Conditions — Optional filters on the trigger
  3. Actions — Steps to execute in sequence

Example Workflow

{
  "name": "Triage Bug Reports",
  "trigger": {
    "type": "submission_created",
    "conditions": {
      "type": "problem",
      "title_contains": "bug"
    }
  },
  "actions": [
    {
      "type": "create_github_issue",
      "params": {
        "repo": "Revoir-Software/revoir-apps",
        "labels": ["bug", "user-reported"],
        "title": "{{submission.title}}",
        "body": "{{submission.description}}\n\nReported by: {{submission.author.email}}"
      }
    },
    {
      "type": "send_email",
      "params": {
        "to": "engineering@revoir.app",
        "subject": "New bug report: {{submission.title}}",
        "template": "bug_report"
      }
    },
    {
      "type": "update_status",
      "params": {
        "status": "under_review"
      }
    }
  ]
}

Triggers

Workflows can trigger on these events:

submission_created

Fires when a user submits feedback.

Conditions:

  • type — Filter by idea or problem
  • category — Filter by category name or ID
  • title_contains — Substring match on title
  • description_contains — Substring match on description

vote_threshold

Fires when a submission reaches a vote count.

Conditions:

  • threshold — Minimum votes (e.g., 10, 50, 100)
  • category — Optional category filter

survey_response

Fires when a user completes a survey.

Conditions:

  • survey_id — Specific survey
  • nps_score — Filter by NPS value (e.g., < 7 for detractors)
  • sentiment — Filter by positive, neutral, negative

status_change

Fires when submission status changes.

Conditions:

  • from_status — Old status
  • to_status — New status

scheduled

Fires on a cron schedule.

Conditions:

  • cron — Cron expression (e.g., 0 9 * * MON for 9am every Monday)

Built-in Actions

send_email

Sends email via Resend.

Params:

  • to — Recipient email (string or array)
  • subject — Email subject
  • template — Named template from lib/email-templates/
  • context — Variables for template rendering

Example:

{
  "type": "send_email",
  "params": {
    "to": "{{submission.author.email}}",
    "subject": "Your feedback is under review",
    "template": "feedback_update",
    "context": {
      "name": "{{submission.author.name}}",
      "title": "{{submission.title}}"
    }
  }
}

create_github_issue

Creates a GitHub issue.

Params:

  • repo — Full repo name (e.g., Revoir-Software/revoir-apps)
  • title — Issue title
  • body — Issue body (supports markdown)
  • labels — Array of label names
  • assignees — Array of GitHub usernames

Example:

{
  "type": "create_github_issue",
  "params": {
    "repo": "Revoir-Software/revoir-apps",
    "title": "{{submission.title}}",
    "body": "{{submission.description}}",
    "labels": ["user-feedback", "{{submission.category}}"]
  }
}

update_status

Updates submission status.

Params:

  • status — New status (enum: open, under_review, planned, in_progress, shipped, declined)

Example:

{
  "type": "update_status",
  "params": {
    "status": "planned"
  }
}

slack_notify

Sends a Slack message.

Params:

  • channel — Channel name (e.g., #product)
  • message — Message text (supports Slack markdown)
  • blocks — Optional Slack Block Kit blocks

Example:

{
  "type": "slack_notify",
  "params": {
    "channel": "#product",
    "message": "New feedback with 10+ votes: {{submission.title}} ({{submission.vote_count}} votes)"
  }
}

Template Variables

Actions support template variables using {{variable}} syntax. Available variables depend on the trigger:

Submission triggers:

  • {{submission.id}}
  • {{submission.title}}
  • {{submission.description}}
  • {{submission.vote_count}}
  • {{submission.author.name}}
  • {{submission.author.email}}
  • {{submission.category}}
  • {{submission.url}}

Survey triggers:

  • {{survey.id}}
  • {{survey.title}}
  • {{response.id}}
  • {{response.nps_score}}
  • {{response.sentiment}}
  • {{response.answers}}

Custom Actions

You can define custom action handlers in lib/workflow/actions/. Each handler implements:

interface ActionHandler {
  type: string
  execute(params: unknown, context: WorkflowContext): Promise<ActionResult>
}

Example custom action:

// lib/workflow/actions/create-zendesk-ticket.ts
export class CreateZendeskTicketAction implements ActionHandler {
  type = 'create_zendesk_ticket'

  async execute(params: unknown, context: WorkflowContext) {
    const { subject, description, priority } = params as {
      subject: string
      description: string
      priority: 'low' | 'normal' | 'high' | 'urgent'
    }

    const ticket = await zendeskClient.tickets.create({
      subject,
      comment: { body: description },
      priority,
    })

    return {
      success: true,
      data: { ticket_id: ticket.id },
    }
  }
}

Register it in lib/workflow/actions/index.ts:

export const actionHandlers = new Map<string, ActionHandler>([
  ['send_email', new SendEmailAction()],
  ['create_github_issue', new CreateGitHubIssueAction()],
  ['create_zendesk_ticket', new CreateZendeskTicketAction()],
  // ...
])

Durable Execution

Workflows use Inngest for durable execution. If an action fails:

  1. Inngest logs the error
  2. Retries with exponential backoff (5 attempts by default)
  3. If all retries fail, marks the workflow as failed
  4. Sends alert to monitoring channel

The workflow state is persisted in Inngest's cloud. You can view execution history in the Inngest dashboard.

Workflow Management

Creating Workflows

Via the admin dashboard:

  1. Navigate to Settings → Workflows
  2. Click Create Workflow
  3. Select a trigger type
  4. Add conditions
  5. Add actions
  6. Save

Via the API:

curl -X POST https://canviq.app/api/admin/workflows \
  -H "Authorization: Bearer your-api-key" \
  -d '{"name": "...", "trigger": {...}, "actions": [...]}'

Listing Workflows

curl https://canviq.app/api/admin/workflows \
  -H "Authorization: Bearer your-api-key"

Triggering Manually

curl -X POST https://canviq.app/api/admin/workflows/:id/trigger \
  -H "Authorization: Bearer your-api-key" \
  -d '{"context": {"submission_id": "..."}}'

What's Next