-- ========================================== -- Chat Switchboard — 018 Workflows -- ========================================== -- Workflow definitions, stages, version snapshots, assignments. -- Consolidated v0.28.0: merges 023 + 025 (assignments) + 027 (webhooks). -- Also adds deferred FK: channels.workflow_id → workflows.id. -- ========================================== -- ========================================= -- WORKFLOW DEFINITIONS -- ========================================= CREATE TABLE IF NOT EXISTS workflows ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), team_id UUID REFERENCES teams(id) ON DELETE CASCADE, name TEXT NOT NULL, slug TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', branding JSONB NOT NULL DEFAULT '{}', entry_mode TEXT NOT NULL DEFAULT 'public_link' CHECK (entry_mode IN ('public_link', 'team_only')), is_active BOOLEAN NOT NULL DEFAULT false, version INTEGER NOT NULL DEFAULT 1, on_complete JSONB, retention JSONB NOT NULL DEFAULT '{"mode": "archive"}', -- Webhooks (v0.27.3) webhook_url TEXT, webhook_secret TEXT, created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug ON workflows(team_id, slug) WHERE team_id IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug ON workflows(slug) WHERE team_id IS NULL; CREATE INDEX IF NOT EXISTS idx_workflows_active ON workflows(is_active) WHERE is_active = true; DROP TRIGGER IF EXISTS workflows_updated_at ON workflows; CREATE TRIGGER workflows_updated_at BEFORE UPDATE ON workflows FOR EACH ROW EXECUTE FUNCTION update_updated_at(); COMMENT ON TABLE workflows IS 'Team-owned or global workflow definitions with staged processes.'; COMMENT ON COLUMN workflows.slug IS 'URL-safe identifier, unique within scope (team or global).'; COMMENT ON COLUMN workflows.branding IS '{"accent_color": "#hex", "logo_url": "...", "tagline": "..."}'; COMMENT ON COLUMN workflows.on_complete IS 'Chaining hook — triggers another workflow on completion.'; COMMENT ON COLUMN workflows.retention IS '{"mode": "archive"|"delete", "delete_after_days": N}'; -- ========================================= -- WORKFLOW STAGES -- ========================================= CREATE TABLE IF NOT EXISTS workflow_stages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, ordinal INTEGER NOT NULL, name TEXT NOT NULL, persona_id UUID REFERENCES personas(id) ON DELETE SET NULL, assignment_team_id UUID REFERENCES teams(id) ON DELETE SET NULL, form_template JSONB NOT NULL DEFAULT '{}', stage_mode TEXT NOT NULL DEFAULT 'chat_only' CHECK (stage_mode IN ('chat_only', 'form_only', 'form_chat', 'review')), history_mode TEXT NOT NULL DEFAULT 'full' CHECK (history_mode IN ('full', 'summary', 'fresh')), auto_transition BOOLEAN NOT NULL DEFAULT false, transition_rules JSONB NOT NULL DEFAULT '{}', surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL, sla_seconds INTEGER, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_workflow_stages_workflow ON workflow_stages(workflow_id, ordinal); COMMENT ON TABLE workflow_stages IS 'Ordered stages within a workflow. Each stage has a driving persona and optional human assignment.'; COMMENT ON COLUMN workflow_stages.history_mode IS 'full=complete history, summary=utility-role summary, fresh=clean slate'; COMMENT ON COLUMN workflow_stages.form_template IS 'JSON schema of fields the persona should collect from the visitor.'; COMMENT ON COLUMN workflow_stages.surface_pkg_id IS 'Optional package that provides a custom stage surface. NULL = built-in surface based on stage_mode.'; COMMENT ON COLUMN workflow_stages.sla_seconds IS 'SLA budget in seconds for this stage. NULL = no SLA.'; -- ========================================= -- WORKFLOW VERSIONS (immutable snapshots) -- ========================================= CREATE TABLE IF NOT EXISTS workflow_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, version_number INTEGER NOT NULL, snapshot JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (workflow_id, version_number) ); CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow ON workflow_versions(workflow_id, version_number DESC); COMMENT ON TABLE workflow_versions IS 'Immutable snapshots of workflow definitions. Active instances run against a pinned version.'; COMMENT ON COLUMN workflow_versions.snapshot IS 'Full JSON: definition + stages + persona tool grants at publish time.'; -- ========================================= -- WORKFLOW ASSIGNMENTS -- ========================================= CREATE TABLE IF NOT EXISTS workflow_assignments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, stage INTEGER NOT NULL, team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE, assigned_to UUID REFERENCES users(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'unassigned' CHECK (status IN ('unassigned', 'claimed', 'completed')), review_comments JSONB NOT NULL DEFAULT '[]', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), claimed_at TIMESTAMPTZ, completed_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_workflow_assignments_channel ON workflow_assignments(channel_id, stage); CREATE INDEX IF NOT EXISTS idx_workflow_assignments_team_status ON workflow_assignments(team_id, status) WHERE status = 'unassigned'; CREATE INDEX IF NOT EXISTS idx_workflow_assignments_user ON workflow_assignments(assigned_to) WHERE assigned_to IS NOT NULL; COMMENT ON TABLE workflow_assignments IS 'Queue entries for human review stages in workflows.'; COMMENT ON COLUMN workflow_assignments.status IS 'unassigned=waiting, claimed=being worked, completed=done'; -- ========================================= -- DEFERRED FK: channels.workflow_id → workflows -- ========================================= DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_workflow' ) THEN ALTER TABLE channels ADD CONSTRAINT fk_channels_workflow FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE SET NULL; END IF; END $$;