Use Case

Support Ticket Forms That Route Themselves

January 2026 · 12 min read

Generic support forms waste everyone's time. Users answer irrelevant questions. Agents manually sort tickets. Issues get lost. Smart forms ask the right questions and route to the right team automatically.

Why Routing Matters

A billing question shouldn't go to engineering. A critical bug shouldn't sit in the general queue. When forms route themselves:

  • Faster resolution - Right team sees it first
  • Better context - Category-specific fields capture what matters
  • Accurate priority - System calculates urgency, not users
  • Clear expectations - Users see where tickets go and when to expect response

Category Selection

Start with a dropdown that drives everything else. Define categories with their routing rules upfront. See the Selection Fields tutorial for dropdown patterns.

const CATEGORIES = [
    { id: 'bug', name: 'Bug Report', team: 'Engineering', priority: 'high' },
    { id: 'billing', name: 'Billing Issue', team: 'Finance', priority: 'high' },
    { id: 'feature', name: 'Feature Request', team: 'Product', priority: 'low' },
    { id: 'account', name: 'Account Help', team: 'Support', priority: 'medium' },
    { id: 'question', name: 'General Question', team: 'Support', priority: 'low' }
];

form.addRow(row => {
    row.addDropdown('category', {
        label: 'What do you need help with?',
        options: CATEGORIES.map(c => ({ id: c.id, name: c.name })),
        placeholder: 'Select a category...',
        isRequired: true
    });
});

Each category has a team assignment. This data structure powers both the routing display and any backend integration.

Show Where Tickets Go

Transparency builds trust. Show users exactly where their ticket will be routed:

// Show where the ticket will be routed
form.addRow(row => {
    row.addTextPanel('routing', {
        computedValue: () => {
            const categoryId = form.dropdown('category')?.value();
            const category = CATEGORIES.find(c => c.id === categoryId);
            if (!category) return '';
            return `→ This will be sent to our ${category.team} team`;
        },
        isVisible: () => form.dropdown('category')?.value() !== null,
        customStyles: {
            color: '#6366f1',
            fontWeight: '500',
            fontSize: '0.9em'
        }
    });
});

The message appears as soon as they select a category. No surprises.

Pro tip

Routing messages reduce "wrong department" complaints. Users self-correct if they picked the wrong category because they see where it's going.

Category-Specific Fields

Different issues need different information. Use conditional subforms to show relevant fields. See the Conditional Visibility tutorial for more patterns.

Bug Reports

Engineers need reproducible bugs. Capture severity, steps, and environment:

// Bug report specific fields
const bugSection = form.addSubform('bugDetails', {
    title: 'Bug Details',
    isVisible: () => form.dropdown('category')?.value() === 'bug'
});

bugSection.addRow(row => {
    row.addDropdown('severity', {
        label: 'Severity',
        options: [
            { id: 'critical', name: 'Critical - System down' },
            { id: 'major', name: 'Major - Feature broken' },
            { id: 'minor', name: 'Minor - Cosmetic issue' }
        ],
        isRequired: true
    });
});

bugSection.addRow(row => {
    row.addTextarea('steps', {
        label: 'Steps to Reproduce',
        placeholder: '1. Go to...\n2. Click on...\n3. See error',
        rows: 4,
        isRequired: true
    });
});

bugSection.addRow(row => {
    row.addTextbox('expected', {
        label: 'Expected Behavior'
    }, '1fr');
    row.addTextbox('actual', {
        label: 'Actual Behavior'
    }, '1fr');
});

bugSection.addRow(row => {
    row.addDropdown('browser', {
        label: 'Browser',
        options: [
            { id: 'chrome', name: 'Chrome' },
            { id: 'firefox', name: 'Firefox' },
            { id: 'safari', name: 'Safari' },
            { id: 'edge', name: 'Edge' },
            { id: 'other', name: 'Other' }
        ]
    });
});

The severity dropdown directly affects priority calculation. "Critical" bugs get urgent attention.

Billing Issues

Finance needs specifics: invoice numbers, amounts, dates:

// Billing issue specific fields
const billingSection = form.addSubform('billingDetails', {
    title: 'Billing Information',
    isVisible: () => form.dropdown('category')?.value() === 'billing'
});

billingSection.addRow(row => {
    row.addDropdown('billingIssue', {
        label: 'Issue Type',
        options: [
            { id: 'charge', name: 'Unexpected charge' },
            { id: 'refund', name: 'Refund request' },
            { id: 'invoice', name: 'Invoice question' },
            { id: 'payment', name: 'Payment failed' },
            { id: 'cancel', name: 'Cancel subscription' }
        ],
        isRequired: true
    });
});

billingSection.addRow(row => {
    row.addTextbox('invoiceNumber', {
        label: 'Invoice Number (if applicable)',
        placeholder: 'INV-XXXXX'
    }, '1fr');
    row.addMoney('amount', {
        label: 'Amount in Question',
        currency: '$'
    }, '1fr');
});

billingSection.addRow(row => {
    row.addDatepicker('chargeDate', {
        label: 'Date of Charge'
    });
});

Different billing issues (refund vs payment failed) may route to different sub-teams or trigger different SLAs.

See conditional forms in action

Feature Requests

Product teams want to understand impact and use cases:

// Feature request specific fields
const featureSection = form.addSubform('featureDetails', {
    title: 'Feature Request',
    isVisible: () => form.dropdown('category')?.value() === 'feature'
});

featureSection.addRow(row => {
    row.addTextbox('featureTitle', {
        label: 'Feature Title',
        placeholder: 'Short description of the feature',
        isRequired: true
    });
});

featureSection.addRow(row => {
    row.addTextarea('useCase', {
        label: 'Use Case',
        placeholder: 'Describe how you would use this feature...',
        rows: 3,
        isRequired: true
    });
});

featureSection.addRow(row => {
    row.addRadioButton('impact', {
        label: 'Business Impact',
        options: [
            { id: 'blocker', name: 'Blocker - Cannot use product without it' },
            { id: 'major', name: 'Major - Significantly improves workflow' },
            { id: 'nice', name: 'Nice to have' }
        ]
    });
});

featureSection.addRow(row => {
    row.addCheckbox('canWorkaround', {
        label: 'I have a workaround for now'
    });
});

The "Business Impact" field helps prioritize the backlog. Blockers get attention; nice-to-haves wait.

Account Issues

Account problems need the account email and specific issue type:

// Account help specific fields
const accountSection = form.addSubform('accountDetails', {
    title: 'Account Information',
    isVisible: () => form.dropdown('category')?.value() === 'account'
});

accountSection.addRow(row => {
    row.addEmail('accountEmail', {
        label: 'Account Email',
        placeholder: 'Email associated with your account',
        isRequired: true
    });
});

accountSection.addRow(row => {
    row.addDropdown('accountIssue', {
        label: 'What do you need help with?',
        options: [
            { id: 'login', name: 'Cannot log in' },
            { id: 'password', name: 'Reset password' },
            { id: '2fa', name: 'Two-factor authentication' },
            { id: 'email', name: 'Change email address' },
            { id: 'delete', name: 'Delete account' }
        ],
        isRequired: true
    });
});

// Show warning for account deletion
accountSection.addRow(row => {
    row.addTextPanel('deleteWarning', {
        computedValue: () => '⚠️ Account deletion is permanent and cannot be undone.',
        isVisible: () => form.dropdown('accountIssue')?.value() === 'delete',
        customStyles: {
            backgroundColor: '#fef2f2',
            color: '#dc2626',
            padding: '12px',
            borderRadius: '6px'
        }
    });
});

Notice the warning for account deletion - conditional styling draws attention to irreversible actions.

Automatic Priority Calculation

Don't ask users "What's the priority?" They'll always say urgent. Calculate it based on their answers. See the Computed Values tutorial for calculation patterns.

// Auto-calculate priority based on inputs
const calculatePriority = form.computedValue(() => {
    const category = form.dropdown('category')?.value();
    const severity = form.dropdown('severity')?.value();
    const billingIssue = form.dropdown('billingIssue')?.value();
    const impact = form.radioButton('impact')?.value();

    // Bug severity drives priority
    if (category === 'bug') {
        if (severity === 'critical') return { level: 'urgent', score: 100 };
        if (severity === 'major') return { level: 'high', score: 75 };
        return { level: 'medium', score: 50 };
    }

    // Billing issues are high priority
    if (category === 'billing') {
        if (billingIssue === 'charge' || billingIssue === 'payment') {
            return { level: 'high', score: 80 };
        }
        return { level: 'medium', score: 60 };
    }

    // Feature requests based on impact
    if (category === 'feature') {
        if (impact === 'blocker') return { level: 'high', score: 70 };
        if (impact === 'major') return { level: 'medium', score: 40 };
        return { level: 'low', score: 20 };
    }

    // Default
    return { level: 'medium', score: 50 };
});

Priority depends on:

  • Bug severity - Critical bugs are urgent
  • Billing type - Payment failures need quick attention
  • Feature impact - Blockers rank higher than nice-to-haves

Display Priority and SLA

Show the calculated priority with visual indicators:

// Display calculated priority
form.addRow(row => {
    row.addTextPanel('priority', {
        computedValue: () => {
            const priority = calculatePriority();
            const icons: Record<string, string> = {
                urgent: '🔴 URGENT',
                high: '🟠 High Priority',
                medium: '🟡 Medium Priority',
                low: '🟢 Low Priority'
            };
            return icons[priority.level] || '';
        },
        isVisible: () => form.dropdown('category')?.value() !== null,
        customStyles: { fontWeight: 'bold', fontSize: '1.1em' }
    });
});

And set expectations with SLA times:

// Show expected response time based on priority
const SLA_TIMES: Record<string, string> = {
    urgent: '< 1 hour',
    high: '< 4 hours',
    medium: '< 24 hours',
    low: '< 48 hours'
};

form.addRow(row => {
    row.addTextPanel('sla', {
        computedValue: () => {
            const priority = calculatePriority();
            const sla = SLA_TIMES[priority.level];
            return `Expected response time: ${sla}`;
        },
        isVisible: () => form.dropdown('category')?.value() !== null,
        customStyles: { color: '#666', fontSize: '0.9em' }
    });
});

Users appreciate knowing when to expect a response. It reduces "just checking in" follow-ups.

Common Fields

Some fields apply to all categories. Add them after the conditional sections:

// Common fields for all categories
form.addSpacer({ height: '16px' });

form.addRow(row => {
    row.addTextbox('subject', {
        label: 'Subject',
        placeholder: 'Brief summary of your issue',
        isRequired: true
    });
});

form.addRow(row => {
    row.addTextarea('description', {
        label: 'Description',
        placeholder: 'Please provide as much detail as possible...',
        rows: 5,
        isRequired: true
    });
});

form.addRow(row => {
    row.addEmail('email', {
        label: 'Your Email',
        tooltip: 'We will respond to this email address',
        isRequired: true
    });
});

Subject, description, and contact email are universal. Keep them at the end so category-specific context comes first.

Complete Example

A minimal self-routing support form:

export function supportTicketForm(form: FormTs) {
    const CATEGORIES = [
        { id: 'bug', name: 'Bug Report', team: 'Engineering' },
        { id: 'billing', name: 'Billing Issue', team: 'Finance' },
        { id: 'feature', name: 'Feature Request', team: 'Product' },
        { id: 'account', name: 'Account Help', team: 'Support' },
        { id: 'question', name: 'General Question', team: 'Support' }
    ];

    // Category selection
    form.addRow(row => {
        row.addDropdown('category', {
            label: 'What do you need help with?',
            options: CATEGORIES.map(c => ({ id: c.id, name: c.name })),
            isRequired: true
        });
    });

    // Routing indicator
    form.addRow(row => {
        row.addTextPanel('routing', {
            computedValue: () => {
                const catId = form.dropdown('category')?.value();
                const cat = CATEGORIES.find(c => c.id === catId);
                return cat ? `→ Routed to: ${cat.team} team` : '';
            },
            isVisible: () => form.dropdown('category')?.value() !== null
        });
    });

    // Bug-specific fields
    const bug = form.addSubform('bug', {
        isVisible: () => form.dropdown('category')?.value() === 'bug'
    });
    bug.addRow(row => {
        row.addDropdown('severity', {
            label: 'Severity',
            options: [
                { id: 'critical', name: 'Critical' },
                { id: 'major', name: 'Major' },
                { id: 'minor', name: 'Minor' }
            ]
        });
    });
    bug.addRow(row => {
        row.addTextarea('steps', { label: 'Steps to Reproduce', rows: 3 });
    });

    // Billing-specific fields
    const billing = form.addSubform('billing', {
        isVisible: () => form.dropdown('category')?.value() === 'billing'
    });
    billing.addRow(row => {
        row.addTextbox('invoice', { label: 'Invoice Number' }, '1fr');
        row.addMoney('amount', { label: 'Amount', currency: '$' }, '1fr');
    });

    // Common fields
    form.addRow(row => {
        row.addTextbox('subject', { label: 'Subject', isRequired: true });
    });
    form.addRow(row => {
        row.addTextarea('description', { label: 'Description', rows: 4 });
    });
    form.addRow(row => {
        row.addEmail('email', { label: 'Your Email', isRequired: true });
    });

    form.configureSubmitButton({ label: 'Submit Ticket' });
}

Backend Integration

The form submission includes all the routing data you need:

  • category - Which team should handle it
  • severity / impact - For priority sorting
  • Category-specific fields - Already structured for your ticketing system

Send to your helpdesk API, create a Jira ticket, or trigger a Slack notification. The form does the categorization; your backend does the routing.

Advanced Patterns

Escalation Triggers

Certain combinations should escalate automatically:

  • Critical bug + enterprise customer = Page on-call
  • Billing issue > $1000 = Manager notification
  • Account deletion request = Retention team review

Add these rules to your priority calculation or handle in the backend based on submission data.

Pre-fill from Context

If users are logged in, pre-fill their email and account info. If they came from a specific page, default the category.

Attachments

Bug reports benefit from screenshots. Billing issues may need invoice copies. Show file upload fields conditionally based on category.

Common Questions

Should I let users override the calculated priority?

Generally no. Users will always pick 'urgent'. If you must allow overrides, require justification and flag tickets where user priority differs from calculated priority for review.

How do I handle tickets that don't fit any category?

Add a 'General Question' or 'Other' category that routes to a triage team. They can re-categorize and route appropriately. Track how often this happens - frequent 'Other' selections may indicate a missing category.

Should category-specific fields be required?

Make critical fields required (severity for bugs, invoice number for billing). Make context fields optional but encouraged. Empty required fields block submission; empty optional fields just mean less context for agents.

How do I integrate with Zendesk/Freshdesk/Intercom?

Use webhooks to send form submissions to your helpdesk API. Map form fields to ticket fields. The category can set the ticket type or tag, and custom fields can capture the category-specific data.

Can I show different SLAs for different customer tiers?

Yes. If you know the customer tier (from login or URL parameter), adjust the SLA display and priority calculation. Enterprise customers might get faster SLAs across all categories.

Ready to Build Smarter Support Forms?

Create forms that route themselves and set proper expectations.