Skip to main content
Routing Forms allow you to ask qualifying questions before someone books a meeting. Based on their answers, you can route them to the appropriate event type, team member, or even prevent booking if they don’t meet criteria.

Overview

A Routing Form is a multi-step form that:
  1. Collects information: Ask custom questions to qualify leads
  2. Evaluates responses: Use conditional logic to determine the best route
  3. Routes to event type: Direct users to the appropriate booking page
  4. Triggers workflows: Execute automations based on form submission
// From schema.prisma:1325
model App_RoutingForms_Form {
  id          String   @id
  name        String   // Form name
  description String?  // Form description
  fields      Json?    // Form fields definition
  routes      Json?    // Routing logic
  disabled    Boolean  @default(false)
  userId      Int      // Owner
  teamId      Int?     // Optional team ownership
  settings    Json?    // Form settings
}
Routing Forms are particularly useful for sales teams, support desks, and any scenario where different types of inquiries need different handling.

Creating a Routing Form

1

Navigate to Routing Forms

Go to Apps → Routing Forms (you may need to install the Routing Forms app first)
2

Create New Form

Click “Create Form” and give it a name
3

Add Fields

Add questions to collect information from users
4

Configure Routes

Set up conditional logic to route based on responses
5

Set Fallback

Define what happens if no routes match
6

Publish Form

Share the form URL with your audience

Form Fields

Routing Forms support various field types:
// From routing-forms types
type Field = {
  id: string;
  type: "text" | "email" | "phone" | "number" | 
        "textarea" | "select" | "multiselect" | 
        "radio" | "checkbox";
  label: string;
  required: boolean;
  placeholder?: string;
  options?: string[];  // For select, radio, checkbox
};
Basic text field for short answers.
{
  type: "text",
  label: "Company Name",
  required: true,
  placeholder: "Acme Corp"
}

Routing Logic

Routes use query-builder logic to evaluate responses:
// Route structure
type Route = {
  id: string;
  queryValue: {  // Conditional logic
    rules: [
      {
        field: "companySize",
        operator: "equals" | "contains" | "greaterThan" | "lessThan",
        value: string | number
      }
    ],
    combinator: "and" | "or"  // How to combine rules
  };
  action: {
    type: "eventTypeRedirect" | "customPageMessage" | "externalRedirect";
    value: string;  // Event type ID, message, or URL
  };
};

Route Actions

Event Type Redirect

Send to a specific event type’s booking page

Custom Message

Display a message (e.g., “We’ll contact you”)

External Redirect

Redirect to any external URL

Example Routing Form

Sales Qualification Form

{
  name: "Sales Inquiry",
  fields: [
    {
      id: "name",
      type: "text",
      label: "Your Name",
      required: true
    },
    {
      id: "email",
      type: "email",
      label: "Email Address",
      required: true
    },
    {
      id: "companySize",
      type: "select",
      label: "Company Size",
      required: true,
      options: ["1-10", "11-50", "51-200", "200+"]
    },
    {
      id: "budget",
      type: "select",
      label: "Monthly Budget",
      required: true,
      options: ["< $1k", "$1k-$5k", "$5k-$10k", "$10k+"]
    },
    {
      id: "timeline",
      type: "radio",
      label: "When do you need to start?",
      options: ["Immediately", "1-3 months", "3-6 months", "Just exploring"]
    }
  ],
  routes: [
    {
      id: "enterprise",
      queryValue: {
        combinator: "and",
        rules: [
          { field: "companySize", operator: "equals", value: "200+" },
          { field: "budget", operator: "equals", value: "$10k+" }
        ]
      },
      action: {
        type: "eventTypeRedirect",
        value: "enterprise-demo"  // Route to enterprise event type
      }
    },
    {
      id: "midmarket",
      queryValue: {
        combinator: "or",
        rules: [
          { field: "companySize", operator: "equals", value: "51-200" },
          { field: "companySize", operator: "equals", value: "11-50" }
        ]
      },
      action: {
        type: "eventTypeRedirect",
        value: "sales-demo"  // Route to standard sales demo
      }
    },
    {
      id: "exploring",
      queryValue: {
        combinator: "and",
        rules: [
          { field: "timeline", operator: "equals", value: "Just exploring" }
        ]
      },
      action: {
        type: "customPageMessage",
        value: "Thanks for your interest! We'll send you more information via email."
      }
    }
  ],
  fallback: {
    type: "eventTypeRedirect",
    value: "consultation-call"  // Default if no routes match
  }
}

Form Responses

// From schema.prisma:1354
model App_RoutingForms_FormResponse {
  id           Int      @id
  formId       String
  response     Json     // User's answers
  createdAt    DateTime @default(now)
  
  routedToBookingUid String?  // If routed to booking
  chosenRouteId      String?  // Which route was taken
}
Form responses are stored and can be viewed in the form’s responses page.

Response Format

// Example response
{
  response: {
    "name": { value: "John Doe", label: "John Doe" },
    "email": { value: "[email protected]", label: "[email protected]" },
    "companySize": { value: "51-200", label: "51-200" },
    "budget": { value: "$5k-$10k", label: "$5k-$10k" },
    "timeline": { value: "1-3 months", label: "1-3 months" }
  },
  chosenRouteId: "midmarket",
  routedToBookingUid: "booking_abc123"
}

Workflow Integration

Routing Forms can trigger workflows:
// From schema.prisma:1610
model WorkflowsOnRoutingForms {
  workflowId    Int
  routingFormId String
  workflow      Workflow
  routingForm   App_RoutingForms_Form
}

Form Workflow Triggers

// From workflows constants.ts:103
enum WorkflowTriggerEvents {
  FORM_SUBMITTED           // When form is submitted with booking
  FORM_SUBMITTED_NO_EVENT  // When form is submitted without booking
}
Workflows can access form response data to personalize notifications based on user answers.

Form Settings

// Form settings
settings: {
  redirectUrl?: string;              // Custom redirect after submission
  emailNotification?: {
    enabled: boolean;
    recipients: string[];            // Who receives notifications
  };
  requireEmailVerification?: boolean;
  reCaptcha?: {
    enabled: boolean;
  };
}

Queued Responses

If a route requires async processing:
// From schema.prisma:1381
model App_RoutingForms_QueuedFormResponse {
  id             String   @id
  formId         String
  response       Json
  chosenRouteId  String?
  fallbackAction Json?    // Action if routing fails
  createdAt      DateTime @default(now)
}
Queued responses are processed by background jobs.

Use Cases

Support Triage

{
  name: "Support Request",
  fields: [
    { type: "select", label: "Issue Type", 
      options: ["Bug", "Feature Request", "Question", "Account Issue"] },
    { type: "select", label: "Severity", 
      options: ["Critical", "High", "Medium", "Low"] }
  ],
  routes: [
    {
      // Critical bugs go to emergency support
      queryValue: { 
        rules: [
          { field: "issueType", operator: "equals", value: "Bug" },
          { field: "severity", operator: "equals", value: "Critical" }
        ],
        combinator: "and"
      },
      action: { type: "eventTypeRedirect", value: "emergency-support" }
    },
    {
      // Account issues to billing team
      queryValue: { 
        rules: [{ field: "issueType", operator: "equals", value: "Account Issue" }] 
      },
      action: { type: "eventTypeRedirect", value: "billing-support" }
    }
  ]
}

Recruitment Screening

{
  name: "Interview Request",
  fields: [
    { type: "text", label: "Position" },
    { type: "select", label: "Years of Experience", 
      options: ["0-2", "3-5", "5-10", "10+"] },
    { type: "radio", label: "Work Authorization",
      options: ["Yes", "No", "Requires Sponsorship"] },
    { type: "select", label: "Availability",
      options: ["Immediate", "2 weeks", "1 month", "2+ months"] }
  ],
  routes: [
    {
      // Senior candidates to senior recruiter
      queryValue: {
        rules: [
          { field: "experience", operator: "in", value: ["5-10", "10+"] }
        ]
      },
      action: { type: "eventTypeRedirect", value: "senior-recruiter" }
    },
    {
      // Junior candidates to junior recruiter
      queryValue: {
        rules: [
          { field: "experience", operator: "in", value: ["0-2", "3-5"] }
        ]
      },
      action: { type: "eventTypeRedirect", value: "junior-recruiter" }
    }
  ]
}

Lead Qualification

{
  name: "Demo Request",
  fields: [
    { type: "email", label: "Work Email" },
    { type: "text", label: "Company" },
    { type: "select", label: "Role", 
      options: ["Developer", "Manager", "Executive", "Other"] },
    { type: "number", label: "Team Size" },
    { type: "checkbox", label: "Which features interest you?",
      options: ["Analytics", "Integrations", "API Access", "Enterprise SSO"] }
  ],
  routes: [
    {
      // Executives with large teams to enterprise sales
      queryValue: {
        combinator: "and",
        rules: [
          { field: "role", operator: "equals", value: "Executive" },
          { field: "teamSize", operator: "greaterThan", value: 50 }
        ]
      },
      action: { type: "eventTypeRedirect", value: "enterprise-sales" }
    },
    {
      // Others to standard demo
      queryValue: { rules: [] },  // Always matches
      action: { type: "eventTypeRedirect", value: "standard-demo" }
    }
  ]
}

Operators Reference

type Operator = 
  | "equals"           // Exact match
  | "notEquals"        // Not equal
  | "contains"         // String contains
  | "notContains"      // String doesn't contain
  | "greaterThan"      // Number >
  | "greaterThanEqual" // Number >=
  | "lessThan"         // Number <
  | "lessThanEqual"    // Number <=
  | "in"               // Value in array
  | "notIn"            // Value not in array
  | "isEmpty"          // Field is empty
  | "isNotEmpty";      // Field has value

Best Practices

Keep Forms Short

Ask only essential qualifying questions. Long forms decrease completion rates.

Provide Clear Options

Use select/radio fields instead of text when possible for easier routing logic.

Set a Fallback

Always define a default action if no routes match.

Test Your Logic

Submit test responses to verify routes work as expected.

Viewing Form Responses

1

Open Routing Form

Go to your routing forms list and select a form
2

View Responses Tab

Click the “Responses” tab to see all submissions
3

Filter and Search

Filter by date, route taken, or specific answer values
4

Export Data

Export responses as CSV for analysis

Advanced: Fallback Actions

When no routes match:
fallback: {
  type: "eventTypeRedirect" | "customPageMessage" | "externalRedirect",
  value: string
}
Always set a fallback action. Users who don’t match any route should still receive a response or next step.

Troubleshooting

Route Not Triggering

  1. Check field names: Ensure route conditions reference correct field IDs
  2. Review operators: Verify operator matches data type (e.g., don’t use greaterThan on strings)
  3. Test logic: Use form preview to test with sample data
  4. Check combinators: Verify and/or logic is correct

Form Not Showing

  1. Check disabled status: Form might be disabled
  2. Verify team membership: For team forms, ensure user has access
  3. Review URL: Confirm form URL is correct
  • Workflows - Automate actions on form submission
  • Event Types - Route to different event types
  • Teams - Team-level routing forms

Build docs developers (and LLMs) love