This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/Workflow/ROADMAP-0.38.md
gobha b7746c3004 Changeset 0.37.14 (#226)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-23 16:47:48 +00:00

20 KiB

ROADMAP-0.38 — Workflow Product Maturity

Everything deferred from v0.37.15 plus what the MVP gate actually requires: "Team admins build workflows visually."

v0.37.15 delivers the plumbing — cancel, unclaim, reassign, stage CRUD in the FE, queue views. This series builds the product on top of that plumbing.


Series Overview

v0.37.15  Workflow Ownership & Lifecycle (plumbing)
v0.37.16  Projects Surface
v0.37.17  Debug / Model Surface
    │
  ── 0.37 tag ──
    │
v0.38.0   Form Builder                    ← team admins stop writing JSON
v0.38.1   Stage Reorder + Bulk Ops        ← queue management at scale
v0.38.2   SLA Alerting                    ← stale queues self-report
v0.38.3   Visitor Experience              ← the public face
v0.38.4   Workflow Templates + Clone      ← reuse across teams
v0.38.5   Analytics Dashboard             ← throughput, bottlenecks, SLA compliance
    │
  ─ ─ ─  MVP v0.50.0 gate  ─ ─ ─

Milestone gates:

Milestone Gate Meaning
v0.37 tag UI Complete Every platform capability has a surface; no features require raw API calls
v0.38.0 Self-service A team admin can build a complete workflow without touching JSON or asking a global admin
v0.38.2 Operational Stale work surfaces automatically; team admins can manage queues without monitoring dashboards
v0.38.5 Measurable Team leads can answer "how long does stage X take" and "where are we bottlenecked"

v0.38.0 — Form Builder

The single biggest gap. form_template is currently raw JSON in a textarea. The MVP gate says "build workflows visually" — this is the make-or-break.

Scope

A visual form builder embedded in the stage editor (E2 from the .15 design). Not a standalone surface — it's a panel that opens when you click "Edit Form" on a form_only or form_chat stage.

Fields supported (matching existing TypedFormTemplate)

text, email, select, number, date, textarea, checkbox, file. Each with the validation rules already defined in models/workflow.go (min/max length, pattern, min/max value, etc).

JSX Exemplar

/**
 * FormBuilder — visual editor for stage form_template
 *
 * Opens as a slide-out panel from StageEditor.
 * Produces a TypedFormTemplate JSON blob on save.
 *
 * Features:
 *   - Add/remove/reorder fields
 *   - Field type picker with type-specific validation config
 *   - Fieldset grouping (progressive multi-step forms)
 *   - Conditional visibility (when/op/value)
 *   - Live preview pane
 */
function FormBuilder({ initial, onSave, onCancel }) {
  const [fields, setFields] = useState(initial?.fields || []);
  const [fieldsets, setFieldsets] = useState(initial?.fieldsets || []);
  const [useFieldsets, setUseFieldsets] = useState((initial?.fieldsets?.length || 0) > 0);
  const [preview, setPreview] = useState(false);

  function addField(targetFieldset) {
    const field = {
      key: `field_${Date.now()}`,
      type: 'text',
      label: '',
      required: false,
    };
    if (useFieldsets && targetFieldset !== undefined) {
      // Add to specific fieldset
      setFieldsets(fs => fs.map((f, i) =>
        i === targetFieldset ? { ...f, fields: [...f.fields, field] } : f
      ));
    } else {
      setFields(f => [...f, field]);
    }
  }

  function save() {
    const template = useFieldsets
      ? { fieldsets, hooks: initial?.hooks || null }
      : { fields, hooks: initial?.hooks || null };
    onSave(template);
  }

  return (
    <div className="form-builder">
      <div className="form-builder__header">
        <h4>Form Builder</h4>
        <label className="toggle-label">
          <input type="checkbox" checked={useFieldsets}
                 onChange={e => setUseFieldsets(e.target.checked)} />
          Multi-step (fieldsets)
        </label>
        <button className="btn-small" onClick={() => setPreview(!preview)}>
          {preview ? 'Edit' : 'Preview'}
        </button>
      </div>

      {preview ? (
        <FormPreview fields={fields} fieldsets={useFieldsets ? fieldsets : null} />
      ) : (
        <div className="form-builder__canvas">
          {useFieldsets ? (
            <FieldsetEditor fieldsets={fieldsets} setFieldsets={setFieldsets} />
          ) : (
            <FieldList fields={fields} setFields={setFields} />
          )}
          <button className="btn-small" onClick={() => addField()}>
            + Add Field
          </button>
        </div>
      )}

      <div className="form-builder__footer">
        <button className="btn-small btn-primary" onClick={save}>Save Form</button>
        <button className="btn-small" onClick={onCancel}>Cancel</button>
      </div>
    </div>
  );
}

/**
 * FieldEditor — single field configuration row
 *
 * Inline editing of key, type, label, required, validation, condition.
 * Type-specific validation options appear contextually.
 */
function FieldEditor({ field, onChange, onRemove, allFields }) {
  function set(k, v) { onChange({ ...field, [k]: v }); }

  return (
    <div className="field-editor">
      <div className="form-row">
        <div className="form-group" style={{ flex: 2 }}>
          <label>Label</label>
          <input value={field.label} onChange={e => set('label', e.target.value)} />
        </div>
        <div className="form-group" style={{ flex: 1 }}>
          <label>Key</label>
          <input value={field.key} onChange={e => set('key', e.target.value)}
                 placeholder="field_name" />
        </div>
        <div className="form-group" style={{ flex: 1 }}>
          <label>Type</label>
          <select value={field.type} onChange={e => set('type', e.target.value)}>
            <option value="text">Text</option>
            <option value="textarea">Textarea</option>
            <option value="email">Email</option>
            <option value="number">Number</option>
            <option value="date">Date</option>
            <option value="select">Select</option>
            <option value="checkbox">Checkbox</option>
            <option value="file">File</option>
          </select>
        </div>
      </div>

      <div className="form-row">
        <label className="toggle-label">
          <input type="checkbox" checked={field.required}
                 onChange={e => set('required', e.target.checked)} />
          Required
        </label>

        {/* Type-specific validation — only render what applies */}
        {(field.type === 'text' || field.type === 'textarea') && (
          <ValidationText validation={field.validation}
                          onChange={v => set('validation', v)} />
        )}
        {field.type === 'number' && (
          <ValidationNumber validation={field.validation}
                            onChange={v => set('validation', v)} />
        )}
        {field.type === 'select' && (
          <OptionsEditor options={field.options || []}
                         onChange={o => set('options', o)} />
        )}
      </div>

      {/* Conditional visibility */}
      <ConditionEditor condition={field.condition}
                       onChange={c => set('condition', c)}
                       allFields={allFields} />

      <button className="btn-small btn-danger" onClick={onRemove}>Remove</button>
    </div>
  );
}

BE changes

None. The form builder is pure FE — it produces the same TypedFormTemplate JSON that already exists. The BE validation in models/workflow.go is unchanged.

Deliverables

File What
src/js/sw/components/form-builder/index.js FormBuilder root
src/js/sw/components/form-builder/field-editor.js FieldEditor, type-specific validators
src/js/sw/components/form-builder/fieldset-editor.js Fieldset grouping
src/js/sw/components/form-builder/condition-editor.js Conditional visibility
src/js/sw/components/form-builder/preview.js Live form preview
src/css/form-builder.css Styles
Integration into StageEditor (E2 from .15) "Edit Form" button opens builder

v0.38.1 — Stage Reorder + Bulk Ops

Stage drag-and-drop

Replace button-based reorder with drag-and-drop in the stage editor. No external library — use the HTML5 Drag and Drop API with draggable, ondragstart, ondragover, ondrop.

The reorder PATCH endpoint already exists: PATCH /api/v1/teams/:teamId/workflows/:wfId/stages/reorder

Bulk assignment operations

Team admins managing 20+ assignments need batch actions.

/**
 * BulkAssignmentBar — selection-based bulk actions
 *
 * Appears above the queue when 1+ assignments are selected.
 * Actions: bulk cancel, bulk reassign (to specific user).
 */
function BulkAssignmentBar({ selected, teamId, onAction }) {
  const [reassignTo, setReassignTo] = useState('');

  async function bulkCancel() {
    const ok = await sw.confirm(`Cancel ${selected.length} assignments?`, true);
    if (!ok) return;
    await Promise.all(selected.map(id => sw.api.workflowAssignments.cancel(id)));
    sw.toast(`${selected.length} cancelled`, 'success');
    onAction();
  }

  async function bulkReassign() {
    if (!reassignTo) return;
    await Promise.all(selected.map(id =>
      sw.api.workflowAssignments.reassign(id, reassignTo)
    ));
    sw.toast(`${selected.length} reassigned`, 'success');
    onAction();
  }

  return (
    <div className="bulk-bar">
      <span>{selected.length} selected</span>
      <button className="btn-small btn-danger" onClick={bulkCancel}>Cancel All</button>
      <div className="bulk-bar__reassign">
        <TeamMemberPicker teamId={teamId} value={reassignTo}
                          onChange={setReassignTo} />
        <button className="btn-small" onClick={bulkReassign}
                disabled={!reassignTo}>
          Reassign
        </button>
      </div>
    </div>
  );
}

BE changes

None. Bulk ops are client-side Promise.all over existing single endpoints. If performance becomes an issue at scale (50+ assignments), add a POST /api/v1/workflow-assignments/bulk endpoint in a future patch. YAGNI for now.


v0.38.2 — SLA Alerting

The monitor already computes sla_breached. This version makes it proactive instead of requiring someone to check the dashboard.

Scheduled SLA scanner

A new periodic task (Go scheduler, not Starlark) that runs every N minutes:

  1. Query active workflow channels with sla_seconds configured
  2. Compute breach status from stage_entered_at
  3. For newly breached instances (not yet notified):
    • Create notification for the assigned team members
    • Emit workflow.sla_breached WS event
    • If webhook_url configured, fire webhook

Data changes

Add sla_notified_at (nullable timestamp) to the channels table. The scanner only fires notifications when sla_notified_at IS NULL and the SLA is breached, then sets the timestamp. Prevents duplicate alerts.

FE changes

The queue items (E1, E4) already render sla_breached badges. Add:

  • Pulsing animation on breached badges
  • Toast on workflow.sla_breached WS event
  • Notification bell integration (already wired in .15 via notifications.NotifyWorkflowClaimed pattern)

Deliverables

File What
server/scheduler/sla_scanner.go Periodic SLA check
server/notifications/workflow_sla.go SLA notification builder
Migration: sla_notified_at column Both PG + SQLite
FE: badge animation CSS src/css/sw-primitives.css

v0.38.3 — Visitor Experience

The visitor currently gets a raw workflow surface. After .15 they get a terminal screen (E5) when their stages are done. This version makes the visitor experience feel like a product.

Scope

  1. Branded entry page — the workflow landing page already supports branding (accent_color, logo_url, tagline). Extend with:

    • Custom welcome text (markdown)
    • Background image/gradient
    • "Powered by" toggle (hide/show Switchboard branding)
  2. Progress indicator — visitor sees "Step 1 of N" for their visible stages only. Internal team stages are invisible. The progress bar counts only stages where the visitor is the audience.

  3. Email notifications — optional. When configured on the workflow:

    • Collect email at stage 0 (or extract from form data)
    • Send "request received" confirmation
    • Send "your request is complete" when workflow completes
    • No notifications for internal stage transitions
  4. Session resume — visitor who closes the browser can return to /w/:id/:slug and resume where they left off. Already partially implemented via sb_session cookie. Polish the UX: show "Welcome back, pick up where you left off" instead of starting fresh.

BE changes

// Workflow model additions:
type WorkflowBranding struct {
    AccentColor   string `json:"accent_color"`
    LogoURL       string `json:"logo_url"`
    Tagline       string `json:"tagline"`
    WelcomeText   string `json:"welcome_text"`    // NEW: markdown
    BackgroundCSS string `json:"background_css"`   // NEW: gradient or image URL
    HidePoweredBy bool   `json:"hide_powered_by"`  // NEW
}

// Email notification config (in workflow settings):
type WorkflowEmailConfig struct {
    Enabled       bool   `json:"enabled"`
    FromName      string `json:"from_name"`
    OnSubmit      string `json:"on_submit_template"`   // markdown template
    OnComplete    string `json:"on_complete_template"`  // markdown template
}

Email sending requires an SMTP config at the platform level (admin settings). If not configured, email toggle is disabled in the workflow editor with a hint.

JSX Exemplar

/**
 * VisitorProgress — step indicator for visitor-facing stages
 *
 * Mount: workflow surface, above the stage content
 * Only counts stages where audience = visitor.
 */
function VisitorProgress({ stages, currentStage }) {
  // Filter to visitor-facing stages only
  const visitorStages = stages.filter(s => !s.assignment_team_id);
  const visitorIndex = visitorStages.findIndex(s => s.ordinal === currentStage);
  const total = visitorStages.length;

  if (total <= 1) return null; // Single stage = no progress bar

  return (
    <div className="visitor-progress">
      {visitorStages.map((s, i) => (
        <div key={s.id}
             className={`visitor-progress__step ${
               i < visitorIndex ? 'done' :
               i === visitorIndex ? 'active' : ''
             }`}>
          <div className="visitor-progress__dot">{i < visitorIndex ? '✓' : i + 1}</div>
          <span className="visitor-progress__label">{s.name}</span>
        </div>
      ))}
    </div>
  );
}

v0.38.4 — Workflow Templates + Clone

Team admins shouldn't rebuild common patterns from scratch. Two features:

Clone workflow

"Duplicate" button on the workflow editor. Creates a new workflow with the same stages, form templates, and routing rules. New slug, new name (appended "- Copy"). No versions carried over (must re-publish).

POST /api/v1/teams/:teamId/workflows/:id/clone
→ 201 { ...newWorkflow }

Pure BE operation — deep-copies workflow + stages, generates new IDs.

Built-in templates

Seed a workflow_templates table with 3-5 common patterns:

Template Stages Description
Customer Intake form → review Public form, team reviews
IT Help Desk LLM chat → triage → resolution AI-assisted intake
Approval Chain form → approve → approve → notify Multi-level approval
Feedback Survey form (multi-step) → analytics Data collection
Onboarding form → team A → team B → team A Multi-team handoff

"New from template" in the team-admin workflow creation flow. Templates are read-only platform data, not user-editable. They serve as starting points — clone and customize.

FE changes

  • "Duplicate" button in workflow editor header
  • "New from Template" option in workflow creation flow
  • Template picker modal with descriptions

v0.38.5 — Analytics Dashboard

Team leads need to answer: how long does each stage take, where are we bottlenecked, what's our SLA compliance rate.

Data source

No new data collection. Everything is derivable from existing columns:

  • channels.created_at — instance start time
  • channels.stage_entered_at — current stage entry
  • channels.workflow_status — terminal state
  • workflow_assignments.created_at / claimed_at / completed_at — assignment lifecycle timestamps

Metrics

Metric Derivation
Throughput Count of completed instances per time period
Average cycle time completed_at - created_at across instances
Stage dwell time next_stage_entered_at - stage_entered_at (requires computing from history)
SLA compliance breached / total per stage with SLA configured
Assignment claim time claimed_at - created_at on assignments
Queue depth over time Snapshot of unassigned count (requires periodic sampling or derive from events)

JSX Exemplar

/**
 * WorkflowAnalytics — team-admin analytics tab
 *
 * Mount: team-admin workflows surface, "Analytics" tab
 * Data: GET /api/v1/teams/:teamId/workflows/analytics
 *        ?workflow_id=...&period=7d|30d|90d
 */
function WorkflowAnalytics({ teamId }) {
  const [data, setData] = useState(null);
  const [workflowId, setWorkflowId] = useState('');
  const [period, setPeriod] = useState('30d');

  // ... load, filter controls ...

  return (
    <div className="wf-analytics">
      <div className="wf-analytics__filters">
        <WorkflowPicker teamId={teamId} value={workflowId} onChange={setWorkflowId} />
        <PeriodPicker value={period} onChange={setPeriod} />
      </div>

      <div className="wf-analytics__cards">
        <StatCard label="Completed" value={data.completed_count} />
        <StatCard label="Avg Cycle Time" value={formatDuration(data.avg_cycle_seconds)} />
        <StatCard label="SLA Compliance" value={`${data.sla_compliance_pct}%`}
                  variant={data.sla_compliance_pct < 80 ? 'danger' : 'success'} />
        <StatCard label="Avg Claim Time" value={formatDuration(data.avg_claim_seconds)} />
      </div>

      {/* Stage funnel with dwell times — uses existing funnel endpoint + dwell data */}
      <StageFunnel stages={data.stage_metrics} />
    </div>
  );
}

BE changes

New endpoint:

GET /api/v1/teams/:teamId/workflows/analytics
    ?workflow_id=...&period=7d|30d|90d

Aggregation query over channels + assignments tables. No new tables. Consider materialized views or periodic snapshot if query cost is too high on large datasets (unlikely at 50-user scale).


Changeset Budget

Version Estimated CS Heaviest piece
v0.38.0 4 Form builder component tree (5+ files)
v0.38.1 2 Drag-and-drop is fiddly but small
v0.38.2 3 SLA scanner + notification plumbing
v0.38.3 4 Visitor branding + email + progress
v0.38.4 2 Clone is one BE endpoint + FE button
v0.38.5 3 Analytics query + chart components
Total ~18

Risk Notes

v0.38.0 (Form Builder) is the long pole. If this slips, the MVP gate phrase "build workflows visually" fails. Prioritize this over everything else in the series. If time is tight, ship a simplified version (no fieldsets, no conditional visibility) and add those in a patch.

v0.38.3 (Email) depends on platform SMTP. If SMTP isn't configured, the feature is invisible. Consider making the email config a prerequisite admin step with a setup wizard, or defer email to v0.38.5 and keep .3 focused on branding + progress.

v0.38.5 (Analytics) can be cut. If the series is running long, analytics is the most deferrable item — it's nice-to-have, not gate-blocking. The existing monitor + funnel endpoints cover the critical "is anything stuck" question.