Paths / Workflow Foundations / Configuration Schemas
Lesson 4 of 5
25 min

Configuration Schemas

Define user inputs, steps, and resource selectors for your workflows.

Learning Objectives

By the end of this lesson, you will be able to:

  • Define workflow inputs using the modern inputs pattern
  • Use appropriate input types: text, number, boolean, select, picker
  • Create dynamic pickers for Notion databases, Slack channels, and other resources
  • Provide sensible defaults that make workflows work out of the box
  • Group related inputs for better user experience

Config schemas define what users customize. Good schemas make workflows flexible without overwhelming users.

Real-World Example: Meeting Intelligence Workflow

Here's how a production workflow (packages/workflows/src/meeting-intelligence/index.ts) defines its inputs:

// From meeting-intelligence/index.ts
inputs: {
  // Core configuration - only these are truly required
  notionDatabaseId: {
    type: 'text',
    label: 'Meeting Notes Database',
    required: true,
    description: 'Where to store meeting notes',
  },
  slackChannel: {
    type: 'text',
    label: 'Meeting Summaries Channel',
    required: true,
    description: 'Where to post meeting summaries',
  },

  // Sync settings - sensible defaults make these optional
  syncMode: {
    type: 'select',
    label: 'What to Sync',
    options: ['meetings_only', 'clips_only', 'both'],
    default: 'both',
    description: 'Sync meetings, clips, or both',
  },
  lookbackDays: {
    type: 'number',
    label: 'Days to Look Back',
    default: 1,
    description: 'How many days of meetings to sync (for daily cron)',
  },

  // AI settings - boolean with sensible default
  enableAI: {
    type: 'boolean',
    label: 'AI Analysis',
    default: true,
    description: 'Extract action items, decisions, and key topics',
  },
  analysisDepth: {
    type: 'select',
    label: 'Analysis Depth',
    options: ['brief', 'standard', 'detailed'],
    default: 'standard',
  },

  // Optional features - disabled by default
  updateCRM: {
    type: 'boolean',
    label: 'Update CRM (HubSpot)',
    default: false,
    description: 'Log meeting activity and update deals in HubSpot',
  },
},

Notice the pattern:

  1. Only 2 required fields (notionDatabaseId, slackChannel)
  2. Everything else has sensible defaults
  3. Optional features default to false
  4. Clear, outcome-focused descriptions

Step-by-Step: Design Your Config Schema

Step 1: Identify Required vs Optional

List everything your workflow needs. Mark each as required or optional:

Required:
- Notion database to save to
- Zoom account connected

Optional (has sensible default):
- Include transcript (default: true)
- Summary format (default: bullets)
- Slack notifications (default: off)

Step 2: Start with Required Fields Only

Add only the essential fields to your schema:

inputs: {
  notionDatabase: {
    type: 'picker',
    pickerType: 'notion:database',
    label: 'Meeting Notes Database',
    description: 'Where your meeting notes will be saved',
    required: true,
  },
},

Step 3: Add Optional Fields with Defaults

Add optional fields with sensible defaults so the workflow works immediately:

inputs: {
  notionDatabase: {
    type: 'picker',
    pickerType: 'notion:database',
    label: 'Meeting Notes Database',
    required: true,
  },
  includeTranscript: {
    type: 'boolean',
    label: 'Include Full Transcript',
    default: true,
  },
  summaryFormat: {
    type: 'select',
    label: 'Summary Style',
    options: [
      { value: 'bullets', label: 'Bullet Points' },
      { value: 'brief', label: 'Brief Summary' },
      { value: 'detailed', label: 'Detailed Summary' },
    ],
    default: 'bullets',
  },
},

Step 4: Add Conditional Fields

Use showIf to reveal advanced options only when needed:

inputs: {
  // ... existing fields
  enableNotifications: {
    type: 'boolean',
    label: 'Send Slack Notifications',
    default: false,
  },
  slackChannel: {
    type: 'picker',
    pickerType: 'slack:channel',
    label: 'Notification Channel',
    showIf: { enableNotifications: true },
  },
},

Step 5: Test the User Experience

Review your schema from the user's perspective:

  1. How many clicks to get started? (aim for 1-2)
  2. Are labels clear without technical jargon?
  3. Do defaults make sense for most users?
  4. Is optional configuration hidden until needed?

The Config Schema

configSchema: {
  fieldName: {
    type: 'text',           // Input type
    label: 'Field Label',   // Shown to user
    description: 'Help text', // Explains the field
    required: true,         // Is it mandatory?
    default: 'value',       // Pre-filled value
  },
}

Input Types

Text Input

For strings, API keys, custom values:

apiKey: {
  type: 'text',
  label: 'API Key',
  description: 'Your service API key',
  required: true,
  secret: true,  // Masks input, encrypts storage
}

customMessage: {
  type: 'text',
  label: 'Welcome Message',
  description: 'Message sent to new subscribers',
  default: 'Welcome to the team!',
  multiline: true,  // Textarea instead of input
}

Number Input

For numeric values with optional bounds:

maxResults: {
  type: 'number',
  label: 'Maximum Results',
  description: 'How many items to process',
  default: 10,
  min: 1,
  max: 100,
}

delayMinutes: {
  type: 'number',
  label: 'Delay (minutes)',
  description: 'Wait time before sending',
  default: 5,
}

Boolean Toggle

For on/off settings:

includeTranscript: {
  type: 'boolean',
  label: 'Include Transcript',
  description: 'Add full meeting transcript to notes',
  default: true,
}

sendNotification: {
  type: 'boolean',
  label: 'Send Slack Notification',
  default: false,
}

Select Dropdown

For predefined options:

priority: {
  type: 'select',
  label: 'Default Priority',
  options: [
    { value: 'low', label: 'Low' },
    { value: 'medium', label: 'Medium' },
    { value: 'high', label: 'High' },
  ],
  default: 'medium',
}

format: {
  type: 'select',
  label: 'Output Format',
  options: [
    { value: 'detailed', label: 'Detailed Summary' },
    { value: 'brief', label: 'Brief Overview' },
    { value: 'bullets', label: 'Bullet Points' },
  ],
}

Picker (Dynamic)

For selecting from user's connected services:

notionDatabase: {
  type: 'picker',
  label: 'Destination Database',
  pickerType: 'notion:database',
  required: true,
}

slackChannel: {
  type: 'picker',
  label: 'Notification Channel',
  pickerType: 'slack:channel',
  required: true,
}

googleCalendar: {
  type: 'picker',
  label: 'Calendar',
  pickerType: 'google-calendar:calendar',
}

Available picker types:

  • notion:database - User's Notion databases
  • notion:page - User's Notion pages
  • slack:channel - Slack channels user can access
  • slack:user - Slack workspace members
  • google-calendar:calendar - User's calendars
  • gmail:label - Gmail labels

Multi-Step Workflows

For workflows with multiple actions, use step-based config:

configSchema: {
  steps: {
    type: 'steps',
    items: [
      {
        id: 'capture',
        name: 'Capture Meeting',
        config: {
          source: {
            type: 'select',
            label: 'Meeting Source',
            options: [
              { value: 'zoom', label: 'Zoom' },
              { value: 'google-meet', label: 'Google Meet' },
            ],
          },
        },
      },
      {
        id: 'save',
        name: 'Save to Notion',
        config: {
          database: {
            type: 'picker',
            pickerType: 'notion:database',
            label: 'Database',
          },
        },
      },
      {
        id: 'notify',
        name: 'Send Notification',
        optional: true,
        config: {
          channel: {
            type: 'picker',
            pickerType: 'slack:channel',
            label: 'Slack Channel',
          },
        },
      },
    ],
  },
}

Conditional Fields

Show fields based on other selections:

configSchema: {
  notificationType: {
    type: 'select',
    label: 'Notification Type',
    options: [
      { value: 'email', label: 'Email' },
      { value: 'slack', label: 'Slack' },
      { value: 'none', label: 'None' },
    ],
  },

  emailAddress: {
    type: 'text',
    label: 'Email Address',
    showIf: { notificationType: 'email' },
  },

  slackChannel: {
    type: 'picker',
    pickerType: 'slack:channel',
    label: 'Slack Channel',
    showIf: { notificationType: 'slack' },
  },
}

Validation

Built-in validation for common patterns:

email: {
  type: 'text',
  label: 'Notification Email',
  validation: {
    pattern: 'email',
  },
}

url: {
  type: 'text',
  label: 'Webhook URL',
  validation: {
    pattern: 'url',
  },
}

custom: {
  type: 'text',
  label: 'Project Code',
  validation: {
    regex: '^PRJ-[0-9]{4}#039;,
    message: 'Must be PRJ- followed by 4 digits',
  },
}

Accessing Config in Execute

async execute({ config }) {
  // Config values are typed and validated
  const database = config.notionDatabase;    // string (picker value)
  const maxResults = config.maxResults;       // number
  const includeTranscript = config.includeTranscript; // boolean

  // Step config for multi-step workflows
  const captureSource = config.steps.capture.source;
  const notifyChannel = config.steps.notify?.channel;  // Optional step
}

Design Principles

1. Sensible Defaults

// Good - works out of box
maxResults: {
  type: 'number',
  default: 10,  // User can adjust, but doesn't have to
}

// Bad - requires decision on install
maxResults: {
  type: 'number',
  required: true,  // Forces user to think about it
}

2. Clear Labels

// Good - clear purpose
{
  label: 'Meeting Notes Database',
  description: 'Where your meeting notes will be saved',
}

// Bad - jargon
{
  label: 'Target DB',
  description: 'Notion database ID for persistence',
}

3. Progressive Disclosure

Put required fields first, optional fields later. Use showIf to hide complexity:

// Required - always visible
notionDatabase: { type: 'picker', required: true },

// Optional - only shown when relevant
customTemplate: {
  type: 'text',
  showIf: { useCustomTemplate: true },
}

4. Minimize Required Fields

Every required field is friction. Can it have a default? Can it be optional?

// Question every 'required: true'
slackChannel: {
  type: 'picker',
  // Is this really required, or just nice to have?
  required: false,  // Make it optional with fallback
}

Complete Examples from Production Workflows

Stripe to Notion Invoice Tracker

From packages/workflows/src/stripe-to-notion/index.ts:

inputs: {
  notionDatabaseId: {
    type: 'text',
    label: 'Notion Database for Payments',
    required: true,
    description: 'Select the database where payments will be logged',
  },
  includeRefunds: {
    type: 'boolean',
    label: 'Track Refunds',
    default: true,
    description: 'Also log refunds to the database',
  },
  currencyFormat: {
    type: 'select',
    label: 'Currency Display',
    options: ['symbol', 'code', 'both'],
    default: 'symbol',
  },
},

Why it works:

  • Only 1 required field (the database)
  • includeRefunds defaults to true because most users want this
  • Simple select options instead of complex object arrays

Client Onboarding Pipeline

From packages/workflows/src/client-onboarding/index.ts:

inputs: {
  notionDatabaseId: {
    type: 'text',
    label: 'Client Database',
    required: true,
    description: 'Notion database to track clients',
  },
  todoistProjectId: {
    type: 'text',
    label: 'Onboarding Tasks Project',
    required: true,
    description: 'Todoist project for onboarding task checklists',
  },
  slackChannel: {
    type: 'text',
    label: 'Team Notifications Channel',
    required: true,
    description: 'Channel for new client alerts',
  },
  onboardingTemplate: {
    type: 'select',
    label: 'Onboarding Template',
    required: true,
    options: [
      { value: 'standard', label: 'Standard (5 tasks)' },
      { value: 'enterprise', label: 'Enterprise (12 tasks)' },
      { value: 'quick-start', label: 'Quick Start (3 tasks)' },
    ],
    default: 'standard',
    description: 'Task template to use for new clients',
  },
  enableAIRecommendations: {
    type: 'boolean',
    label: 'AI Onboarding Recommendations',
    default: true,
    description: 'Use AI to generate custom onboarding steps based on client profile',
  },
  assigneeEmail: {
    type: 'email',
    label: 'Default Account Manager',
    required: false,
    description: 'Email of team member to assign onboarding tasks',
  },
},

Notice:

  • Select options use { value, label } format for clarity
  • Optional fields like assigneeEmail use required: false
  • AI features default to true (users can disable if needed)

Dental Appointment Autopilot

From packages/workflows/src/dental-appointment-autopilot/index.ts:

inputs: {
  practiceId: {
    type: 'text',
    label: 'Sikka Practice ID',
    required: true,
    description: 'Your practice ID from Sikka portal',
  },
  slackChannel: {
    type: 'text',
    label: 'Staff Notification Channel',
    required: false,
    description: 'Channel for staff alerts (cancellations, no-show risks)',
  },
  reminder48h: {
    type: 'boolean',
    label: '48-Hour Reminder',
    default: true,
    description: 'Send reminder 48 hours before appointment',
  },
  reminder24h: {
    type: 'boolean',
    label: '24-Hour Reminder',
    default: true,
    description: 'Send reminder 24 hours before appointment',
  },
  reminder2h: {
    type: 'boolean',
    label: '2-Hour Reminder',
    default: true,
    description: 'Send reminder 2 hours before appointment',
  },
  enableWaitlistBackfill: {
    type: 'boolean',
    label: 'Waitlist Backfill',
    default: true,
    description: 'Automatically offer canceled slots to waitlist patients',
  },
  highRiskThreshold: {
    type: 'number',
    label: 'No-Show Risk Threshold',
    default: 70,
    description: 'Alert staff when no-show probability exceeds this % (based on history)',
  },
  messageTemplate: {
    type: 'textarea',
    label: 'Reminder Message Template',
    default: 'Hi {{firstName}}, this is a reminder of your dental appointment...',
    description: 'Template for SMS/email reminders. Use {{placeholders}}.',
  },
},

Pattern insights:

  • Boolean groups for related features (reminder48h, reminder24h, reminder2h)
  • textarea type for multi-line template content
  • Number with business-meaningful default (70% threshold)
  • Template placeholders documented in description

Smart Defaults Pattern (Zuhandenheit)

Production workflows use the pathway.smartDefaults and pathway.essentialFields pattern to minimize required configuration:

// From meeting-intelligence/index.ts
pathway: {
  // Only these 1-2 fields are required for first activation
  essentialFields: ['notionDatabaseId'],

  // Smart defaults - infer from context, don't ask
  smartDefaults: {
    syncMode: { value: 'both' },
    lookbackDays: { value: 1 },
    transcriptMode: { value: 'prefer_speakers' },
    enableAI: { value: true },
    analysisDepth: { value: 'standard' },
    postToSlack: { value: true },
    updateCRM: { value: false },
  },

  zuhandenheit: {
    timeToValue: 3, // Minutes to first outcome
    worksOutOfBox: true, // Works with just essential fields
    gracefulDegradation: true, // Optional integrations handled gracefully
    automaticTrigger: true, // Webhook-triggered, no manual invocation
  },
},

The philosophy:

  • essentialFields defines what users MUST configure
  • smartDefaults pre-fills everything else
  • zuhandenheit.worksOutOfBox: true means the workflow functions immediately

From packages/workflows/src/sales-lead-pipeline/index.ts:

// Weniger, aber besser: Only 2 essential fields
// Smart defaults handle enableCRM, enableAIScoring, followUpDays
// Optional integrations auto-detected from connected services
inputs: {
  typeformId: {
    type: 'text',
    label: 'Lead Capture Form',
    required: true,
    description: 'Select the Typeform that captures leads',
  },
  slackChannel: {
    type: 'text',
    label: 'Sales Notifications Channel',
    required: true,
    description: 'Channel where lead alerts will be posted',
  },
  // Optional: Only shown if Todoist connected and user wants to customize
  todoistProjectId: {
    type: 'text',
    label: 'Follow-up Tasks Project',
    required: false,
    description: 'Auto-detects first project if not specified',
  },
},

Key insight: The workflow auto-detects the first Todoist project if none specified. The tool recedes; the outcome remains.

Common Pitfalls

Too Many Required Fields

Every required field is friction. Users abandon complex setups:

// Wrong - overwhelming setup
inputs: {
  notionDatabase: { type: 'picker', required: true },
  slackChannel: { type: 'picker', required: true },
  emailRecipient: { type: 'text', required: true },
  scheduleTime: { type: 'text', required: true },
  timezone: { type: 'select', required: true },
  format: { type: 'select', required: true },
}

// Right - essential only, smart defaults for rest
inputs: {
  notionDatabase: { type: 'picker', required: true },  // Essential
  slackChannel: { type: 'picker', required: false, description: 'Optional notifications' },
  format: { type: 'select', default: 'bullets' },  // Has default
}

Missing Default Values

Force users to make decisions they shouldn't need to:

// Wrong - requires user to choose
maxResults: {
  type: 'number',
  label: 'Maximum Results',
  required: true,  // User must pick a number
}

// Right - sensible default
maxResults: {
  type: 'number',
  label: 'Maximum Results',
  default: 10,  // Works out of the box
  description: 'Adjust if needed',
}

Jargon in Labels

Use outcome-focused language, not technical terms:

// Wrong - developer speak
{
  label: 'Notion DB UUID',
  description: 'The database_id parameter for page creation',
}

// Right - user outcome
{
  label: 'Meeting Notes Database',
  description: 'Where your meeting summaries will appear',
}

Type Mismatch in Execute

Config values have specific types - don't assume:

// Wrong - assumes string is number
inputs: {
  maxItems: { type: 'text', label: 'Max Items' },
}
async execute({ inputs }) {
  for (let i = 0; i < inputs.maxItems; i++) {  // "10" !== 10
    // ...
  }
}

// Right - use number type
inputs: {
  maxItems: { type: 'number', label: 'Max Items', default: 10 },
}
async execute({ inputs }) {
  for (let i = 0; i < inputs.maxItems; i++) {  // 10 is number
    // ...
  }
}

Forgetting showIf Dependencies

Conditional fields need their dependency to exist:

// Wrong - showIf references non-existent field
inputs: {
  slackChannel: {
    type: 'picker',
    showIf: { enableNotifications: true },  // enableNotifications not defined!
  },
}

// Right - define the controlling field
inputs: {
  enableNotifications: {
    type: 'boolean',
    label: 'Enable Notifications',
    default: false,
  },
  slackChannel: {
    type: 'picker',
    showIf: { enableNotifications: true },  // Now works
  },
}

Not Validating Picker Values

Pickers return IDs that might become invalid:

// Wrong - assumes picker value always valid
async execute({ inputs, integrations }) {
  await integrations.notion.pages.create({
    parent: { database_id: inputs.notionDatabase },  // What if deleted?
  });
}

// Right - handle missing/invalid
import { ErrorCode } from '@workwayco/sdk';

async execute({ inputs, integrations }) {
  const result = await integrations.notion.pages.create({
    parent: { database_id: inputs.notionDatabase },
  });
  if (!result.success && result.error?.code === ErrorCode.NOT_FOUND) {
    return {
      success: false,
      error: 'The selected database no longer exists. Please reconfigure.',
    };
  }
}

Praxis

Design a config schema for one of your outcome statements from the earlier lesson:

Praxis: Ask Claude Code: "Help me design a configSchema for a workflow that [your outcome]"

Follow these principles:

  1. Start with the minimum required fields
  2. Add sensible defaults for everything optional
  3. Use pickers where possible instead of text input
  4. Add showIf for conditional fields

Example for a meeting notes workflow:

configSchema: {
  notionDatabase: {
    type: 'picker',
    pickerType: 'notion:database',
    label: 'Meeting Notes Database',
    required: true,
  },
  includeSummary: {
    type: 'boolean',
    label: 'AI Summary',
    default: true,
  },
  slackChannel: {
    type: 'picker',
    pickerType: 'slack:channel',
    label: 'Notification Channel',
    showIf: { enableNotifications: true },
  },
}

Count your required fields. Can any become optional with defaults?

Reflection

  • How do good defaults reduce friction for users?
  • What's the minimum configuration your workflow actually needs?
  • How does progressive disclosure help the interface recede?

Praxis — Try it with Claude Code

Design a configSchema for one of your outcome statements. Use sensible defaults and minimize required fields.

Open your terminal and ask Claude Code. The learning happens in the doing.

Sign in to track progress