CaseSolo Command Graph RFC
Status: Proposed for implementation Updated: 2026-06-03 Scope: Normalized events, actions, permissions, idempotency, approvals, replay, delivery, and audit for CaseSolo Automate AI and legal system-of-action workflows.
Executive Summary
CaseSolo already has automation primitives, workflow nodes, webhook delivery, and audit logging. They are not yet one coherent execution layer. Today, emitAutomationEvent() fans out limited events to webhooks and workflow triggers, workflow node handlers can mutate Prisma models directly, webhook delivery attempts are stored in firm settings JSON, and audit logging is activity-based rather than command-coupled.
The Command Graph turns those pieces into a single legal-native control plane:
- Every domain event is normalized, tenant-scoped, correlated, and replay-aware.
- Every action runs through policy, idempotency, approval, execution, receipt, and audit stages.
- AI can propose work, but high-risk legal, financial, document, admin, and integration mutations require human approval.
- Webhook, workflow, AI, portal, and future adapter actions share the same command receipt and audit trail.
- Failed work becomes observable and replayable without bypassing permissions.
This RFC is architectural. It defines the contract and implementation waves; it does not claim the graph is already implemented.
Current Repo Fit
Current primitives that should be migrated into the graph:
| Area | Current file | Current role | Command Graph gap | |---|---|---|---| | Automation events | src/lib/automation/events.ts | Emits a small set of case/task/document events to webhooks and workflows | No common event envelope, correlation ID, idempotency key, policy decision, or durable event ledger | | Workflow execution | src/lib/workflow/engine.ts | Traverses workflow nodes and executes inline action handlers | Actions mutate directly and are not uniformly tenant-safe, approved, replayable, or audited | | Workflow triggers | src/lib/workflow/triggers.ts | Finds active workflows by firm and trigger type | Trigger matching is separate from webhook delivery and does not produce a durable command event | | Webhook delivery | src/lib/webhooks/deliver.ts | HMAC-signs and retries outbound webhook calls | Delivery attempts are JSON-backed and not first-class replay/audit objects | | Audit logging | src/lib/audit/log.ts | Writes audit sentinel metadata into Activity | Audit is fire-and-forget and not tied to a command lifecycle, policy result, approval, or before/after diff | | Activity timeline | prisma/schema.prisma model Activity | User-facing event timeline | Good timeline surface, but not enough as the immutable command source of truth | | Workflow run trace | prisma/schema.prisma model WorkflowRun | Stores workflow run status and log JSON | Needs step-level command execution IDs, approvals, replay, and failure recovery | | Webhook endpoint | prisma/schema.prisma model WebhookEndpoint | First-class endpoint model exists | Delivery attempts and replay need first-class tables too |
Notable current risk to eliminate: action_update_status in src/lib/workflow/engine.ts updates a case by id only. A Command Graph action handler must enforce firmId, actor permissions, idempotency, approval policy, and immutable audit before any status mutation executes.
Goals
- Provide one normalized event/action layer across cases, contacts, tasks, documents, billing, trust, calendar, communications, medical records, court events, portal actions, AI, God Mode, and integrations.
- Make command execution tenant-safe by construction.
- Require policy checks before every mutation, including AI and workflow-originated mutations.
- Support deterministic idempotency for retries, webhooks, workflow reruns, and provider callbacks.
- Add approval gates for high-risk legal, financial, document, admin, and integration actions.
- Make replay explicit, permissioned, audited, and safe.
- Produce a durable audit record with causation, correlation, before/after snapshots, policy decision, approval state, and execution receipt.
- Give the Integration Reliability Center a real event/delivery/run substrate.
Non-Goals
- This RFC does not replace every existing workflow or route immediately.
- This RFC does not allow autonomous AI to silently mutate high-risk legal records.
- This RFC does not change external provider protocols by itself.
- This RFC does not make every command reversible; some commands are only compensatable or non-replayable.
- This RFC does not remove
Activity; user-facing timelines can continue to read fromActivitywhile command audit becomes the source of truth.
Terminology
| Term | Meaning | |---|---| | DomainEvent | A factual thing that happened, such as case.status_changed or document.uploaded | | CommandIntent | A requested action that may mutate state or call an external system | | CommandAction | A registered executable action type, such as case.update_status | | PolicyDecision | The authorization, tenant, plan, object, and risk decision for an intent | | ApprovalRequest | A human review object required before high-risk actions execute | | CommandExecution | The durable lifecycle record for a command intent | | CommandReceipt | The stable result returned to callers after execution or deduplication | | DeliveryAttempt | A first-class record for outbound provider/webhook delivery attempts | | AuditRecord | The append-only record of accepted command lifecycle state | | Correlation ID | Groups a workflow, request, AI run, import, or replay chain | | Causation ID | Points to the immediate parent event, command, or approval | | Idempotency Key | A deterministic key that deduplicates retries and preserves receipts |
Event Envelope
All graph events should validate through Zod and be JSON-serializable.
export type CommandGraphActor = {
type: 'user' | 'portal_client' | 'system' | 'ai' | 'integration' | 'god_mode'
id: string | null
impersonatorId?: string | null
reason?: string | null
}
export type CommandGraphSubject = {
type:
| 'Case'
| 'Task'
| 'Document'
| 'Contact'
| 'Invoice'
| 'TrustTransaction'
| 'CalendarEvent'
| 'Communication'
| 'Workflow'
| 'WebhookEndpoint'
| 'Integration'
| 'Firm'
| 'User'
id: string
}
export type CommandGraphEvent = {
id: string
type: string
version: 1
firmId: string
caseId?: string | null
actor: CommandGraphActor
subject: CommandGraphSubject
occurredAt: string
source: {
kind: 'api' | 'workflow' | 'webhook' | 'ai' | 'cron' | 'portal' | 'migration' | 'god_mode'
requestId?: string | null
route?: string | null
provider?: string | null
}
payload: Record<string, unknown>
permissions: {
role?: string | null
scopes: string[]
}
correlationId: string
causationId?: string | null
idempotencyKey: string
}
Required event types for the first implementation wave:
| Namespace | Examples | |---|---| | Case | case.created, case.updated, case.status_changed, case.assigned, case.sol_approaching | | Task | task.created, task.completed, task.overdue, task.assigned | | Document | document.upload_requested, document.upload_completed, document.shared, document.deleted, document.ai_review_requested | | Billing and trust | invoice.created, payment.received, trust_transaction.created, expense.created | | Calendar | calendar.event_created, calendar.event_synced, deadline.created, deadline.missed | | Communications | communication.logged, message.sent, sms.received, email.received | | Workflow | workflow.published, workflow.run_started, workflow.run_failed, workflow.rollback_requested | | Webhook and integrations | webhook.delivery_failed, webhook.replay_requested, integration.oauth_expiring, integration.sync_failed | | AI | ai.run_started, ai.output_ready, ai.approval_requested, ai.action_approved, ai.action_rejected | | God Mode | god_mode.impersonation_started, god_mode.support_action_requested, god_mode.feature_flag_changed |
Command Intent Envelope
Every mutating workflow, API route, AI action, webhook replay, and integration adapter should produce a command intent.
export type CommandIntent = {
id: string
action: string
version: 1
firmId: string
caseId?: string | null
actor: CommandGraphActor
target: CommandGraphSubject
input: Record<string, unknown>
reason?: string | null
requiresApproval: boolean
risk:
| 'low'
| 'medium'
| 'high'
| 'critical'
dryRun?: boolean
expectedVersion?: string | null
idempotencyKey: string
correlationId: string
causationId?: string | null
requestedAt: string
}
Initial action registry:
| Domain | Action examples | |---|---| | Cases | case.create, case.update_status, case.assign_user, case.add_party, case.update_metadata | | Tasks | task.create, task.complete, task.reopen, task.assign, task.bulk_update | | Documents | document.request_upload, document.complete_upload, document.share, document.delete, document.request_signature | | AI | ai.request_analysis, ai.submit_output_for_approval, ai.apply_approved_output, ai.reject_output | | Workflow | workflow.publish, workflow.rollback, workflow.run, workflow.cancel_run | | Webhook | webhook.create_endpoint, webhook.test_delivery, webhook.replay_delivery, webhook.disable_endpoint | | Integrations | integration.connect, integration.sync, integration.disconnect, integration.replay_failed_action | | Billing | invoice.create, payment.record, expense.create, trust_transaction.create | | God Mode | god_mode.impersonate, god_mode.update_firm_plan, god_mode.force_logout_user, god_mode.toggle_feature_flag |
Policy Decision Model
Policy must run before execution and before approval creation. A policy decision should be stored even when the command is denied.
Policy inputs:
- Actor type, user ID, portal client ID, impersonator ID, role, and scopes.
- Firm ID and target object firm ownership.
- Case membership or client portal case access.
- Plan entitlements, billing status, feature flags, and usage limits.
- Target object status, expected version, locked/finalized state, and legal hold.
- Action risk rating and whether approval is required.
- Source kind, request route, IP, user agent, provider, and CSRF/auth posture.
- AI provenance, source spans, confidence, and tool permissions when actor type is
ai.
Policy outputs:
export type PolicyDecision = {
decision: 'allow' | 'deny' | 'needs_approval'
reasons: string[]
matchedRules: string[]
requiredApprovalRole?: 'attorney' | 'firm_admin' | 'super_admin'
redactions: string[]
evaluatedAt: string
}
Mandatory policy rules:
- Tenant ownership is non-negotiable: every target read/write must prove
firmId. - Portal actors can only act on explicitly linked portal/client records.
- God Mode impersonation requires reason, max duration, banner, and immutable audit.
- AI actors cannot directly execute high-risk legal, financial, document deletion, provider-send, workflow publish, webhook replay, or God Mode actions.
- Financial, trust, settlement, document deletion, workflow publish/rollback, webhook replay, and impersonation actions require approval.
- External provider calls must respect rate limits, idempotency, credential health, and provider-specific safety rules.
Idempotency
Command idempotency must be deterministic and receipt-preserving.
Recommended key format:
firm:{firmId}:action:{action}:target:{targetType}:{targetId}:actor:{actorId}:intent:{canonicalInputHash}
Rules:
- Canonicalize input by sorting object keys, removing transient fields, normalizing dates to ISO strings, and excluding secrets.
- Hash canonical input with SHA-256.
- Store a unique
(firmId, idempotencyKey)onCommandExecution. - A duplicate request returns the original command receipt, not a second mutation.
- Idempotency applies to API retries, workflow reruns, webhook callbacks, provider retries, and replay operations.
- Keys must include actor or explicit correlation scope when two actors can validly perform the same action on the same target.
Execution statuses:
RECEIVED -> VALIDATED -> NEEDS_APPROVAL -> APPROVED -> RUNNING -> SUCCEEDED
| | |-> FAILED
| |-> CANCELED
|-> DENIED
SUCCEEDED -> REPLAY_REQUESTED -> REPLAYED
FAILED -> REPLAY_REQUESTED -> RUNNING
Approval Gates
Approval is a first-class command state, not a UI-only confirmation.
Approval request fields:
id,firmId,commandExecutionId,requestedBy,requiredRole,status.risk,reason,policyDecision,inputSummary,impactedRecords.aiProvenancewhen generated by AI.expiresAt,approvedBy,rejectedBy,decisionReason,decidedAt.correlationId,causationId,requestId.
Actions that require approval by default:
- AI-generated legal work that mutates a case, document, deadline, settlement, or client-facing message.
- Settlement authority changes, financial transactions, trust ledger entries, invoice finalization, and payment recording.
- Document deletion, public sharing, signature envelope send, and external document delivery.
- Workflow publish, rollback, and automation activation that can mutate records or call providers.
- Webhook replay and integration replay that can deliver data externally.
- God Mode impersonation, firm plan changes, forced session logout, feature flag changes, and destructive platform actions.
Approval UI requirements:
- Show source event, command action, target, proposed changes, before/after summary, risk reason, and audit impact.
- Show AI citations, source spans, confidence, model, tool calls, and permission scope for AI-originated actions.
- Allow approve, reject, request changes, dry-run preview, and view impacted workflows.
- Never hide that an action originated from AI, automation, webhook replay, or God Mode.
Replay
Replay is a controlled action, not a retry button that bypasses policy.
Replay rules:
- Replay creates a new command intent with
source.kind = 'replay'or a replay flag in source metadata. - Replay preserves the original
correlationIdand records the original execution ID ascausationId. - Replay requires a new actor, reason, timestamp, and policy decision.
- Replay can reuse the original payload only after validation against the current schema version.
- Replay of high-risk or external-delivery actions requires approval.
- Non-replayable commands must return a clear reason and suggested compensating action.
- Replayed commands must write both a replay audit record and the resulting command audit record.
Replay classes:
| Class | Examples | Rule | |---|---|---| | Idempotent | Recalculate dashboard metric, resend safe notification to same target with same delivery ID | Can return original receipt or run with explicit replay receipt | | Retryable | Failed webhook delivery, OAuth sync page, provider fetch | Can replay after provider health and policy checks | | Compensatable | Status update, task create, document share | Replay only if current state still matches expected version or user approves a compensating action | | Non-replayable | Trust ledger finalization, irreversible external send, destructive admin action | Reject replay unless an approved compensating command exists |
Audit Model
Command audit should become the durable evidence layer. Activity can remain the human-readable timeline.
Audit requirements:
- Write an audit record when a command is received, denied, approved, executed, failed, replayed, canceled, or compensated.
- Store actor, firm, target, source, route/request metadata, correlation ID, causation ID, idempotency key, policy decision, approval state, and command receipt.
- Store before/after snapshots or redacted diffs for mutating actions.
- Redact secrets, provider tokens, webhook secrets, file bytes, full PHI payloads where not necessary, and auth credentials.
- Preserve immutable semantics with append-only rows. Future wave can add hash chaining.
- Every command execution should be queryable by firm, case, target, actor, status, correlation ID, and time.
Activity projection:
- User-facing timelines can receive projected entries from command events, such as
case.status_changedordocument.shared. - Projection failures must not erase the command audit record.
- Activity metadata should include
commandExecutionIdandcorrelationIdonce the graph exists.
Proposed Data Model
These are conceptual Prisma model targets. Final schema names can change during migration.
model CommandEvent {
id String @id @default(cuid())
firmId String
caseId String?
type String
version Int @default(1)
actor Json
subject Json
source Json
payload Json
permissions Json
correlationId String
causationId String?
idempotencyKey String
occurredAt DateTime
createdAt DateTime @default(now())
@@index([firmId, createdAt])
@@index([firmId, type])
@@index([caseId])
@@index([correlationId])
}
model CommandExecution {
id String @id @default(cuid())
firmId String
caseId String?
action String
version Int @default(1)
actor Json
target Json
input Json
inputHash String
risk String
status String
policyDecision Json?
receipt Json?
error String?
correlationId String
causationId String?
idempotencyKey String
requestedAt DateTime
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([firmId, idempotencyKey])
@@index([firmId, status])
@@index([firmId, createdAt])
@@index([caseId])
@@index([correlationId])
}
model CommandApproval {
id String @id @default(cuid())
firmId String
commandExecutionId String
status String
requiredRole String
requestedBy String?
decidedBy String?
decisionReason String?
inputSummary Json
impactedRecords Json
expiresAt DateTime?
decidedAt DateTime?
createdAt DateTime @default(now())
@@index([firmId, status])
@@index([commandExecutionId])
}
model CommandDeliveryAttempt {
id String @id @default(cuid())
firmId String
commandExecutionId String?
endpointId String?
provider String
eventType String
status String
attempt Int
requestHeaders Json
responseSummary Json?
redactedPayload Json
correlationId String
createdAt DateTime @default(now())
completedAt DateTime?
@@index([firmId, status])
@@index([firmId, createdAt])
@@index([correlationId])
}
model CommandAuditRecord {
id String @id @default(cuid())
firmId String
caseId String?
commandExecutionId String?
eventId String?
action String
actor Json
target Json
source Json
policyDecision Json?
approval Json?
before Json?
after Json?
diff Json?
receipt Json?
correlationId String
causationId String?
createdAt DateTime @default(now())
@@index([firmId, createdAt])
@@index([caseId])
@@index([commandExecutionId])
@@index([correlationId])
}
API Surface
Next.js Route Handlers are the right server boundary for the public/internal graph API. Per the local Next.js 16 route-handler guide, route handlers use Web Request/Response, live under app, and non-GET methods are not cached by default. Graph endpoints should still explicitly set auth, CSRF, rate-limit, and no-store semantics where applicable.
Recommended endpoints:
| Route | Method | Purpose | |---|---|---| | /api/command-graph/events | POST | Ingest a normalized event from API, workflow, AI, cron, portal, webhook, or integration source | | /api/command-graph/actions | GET | List registered actions, schemas, risk, approval policy, and required permissions | | /api/command-graph/intents | POST | Submit a command intent for validation, policy, approval, or execution | | /api/command-graph/executions/[id] | GET | Fetch execution status, receipt, policy decision, approval state, delivery attempts, and audit links | | /api/command-graph/approvals | GET | List pending approvals by firm, case, risk, action, and required role | | /api/command-graph/approvals/[id] | POST | Approve, reject, request changes, or expire an approval | | /api/command-graph/replay | POST | Replay a failed or approved replayable execution with a new reason and actor | | /api/command-graph/dry-run | POST | Validate and preview policy, impacted records, and side effects without mutation |
Response shape:
type CommandReceipt = {
commandExecutionId: string
status:
| 'DENIED'
| 'NEEDS_APPROVAL'
| 'APPROVED'
| 'RUNNING'
| 'SUCCEEDED'
| 'FAILED'
| 'CANCELED'
| 'REPLAYED'
idempotencyKey: string
correlationId: string
target: CommandGraphSubject
result?: Record<string, unknown>
error?: {
code: string
message: string
details?: Record<string, unknown>
}
approvalId?: string
auditRecordId?: string
}
Library Layout
Recommended first implementation layout:
src/lib/command-graph/
actions.ts # action registry
actor.ts # actor normalization from staff, portal, AI, integration, God Mode
audit.ts # append-only audit writes and Activity projection
approvals.ts # approval policy, lifecycle, and queues
delivery.ts # delivery-attempt abstraction for webhooks/providers
events.ts # event schemas and ingestion
executor.ts # orchestration from intent to receipt
idempotency.ts # canonicalization, hashing, duplicate receipt lookup
policy.ts # RBAC, tenant, object, plan, AI, and risk policies
replay.ts # replay eligibility and command cloning
schemas.ts # Zod schemas and DTO exports
telemetry.ts # Sentry/PostHog/log events
adapters/
workflow.ts # workflow node adapter
webhook.ts # webhook adapter
ai.ts # AI proposal adapter
portal.ts # portal action adapter
Workflow Migration Plan
Wave 1: RFC and schemas
- Land this RFC and planning companion.
- Add Zod schemas and TypeScript DTOs for events, intents, policy decisions, receipts, approvals, and replay.
- Add unit tests for schema validation and idempotency canonicalization.
Wave 2: Event intake and audit
- Wrap
emitAutomationEvent()so every existing automation event becomes aCommandEvent. - Project accepted events to existing webhook/workflow behavior to preserve current functionality.
- Add command audit records and Activity projection for graph-backed events.
Wave 3: Action registry
- Register low-risk actions first: task create, task complete, notification create, safe dashboard recalculation, webhook test delivery.
- Add policy tests for tenant safety, portal access, God Mode impersonation, and AI restrictions.
Wave 4: Workflow engine adapter
- Replace inline workflow action mutations with
CommandIntentsubmissions. - Record
commandExecutionIdon workflow run log entries. - Block direct workflow mutation handlers that do not prove firm scope.
Wave 5: Approvals and AI review
- Add approval queue UI and API.
- Route AI-generated legal work into approvals before any case/document/deadline mutation.
- Add source spans, citations, confidence, and model/tool metadata to approval summaries.
Wave 6: Replay and reliability center
- Move webhook delivery attempts to first-class records.
- Add replay UI for failed webhook/integration/workflow actions.
- Surface OAuth health, token expiry, last sync, backlog, failed jobs, and impacted workflows.
Security Requirements
- All mutation handlers must prove
firmIdownership through scoped queries or tenant-safe helpers. - All external inbound mutation requests must pass auth, signature verification where applicable, CSRF policy where applicable, and rate limits.
- All outbound delivery logs must redact secrets, auth headers, tokens, PHI-heavy payloads, and full provider responses.
- All AI-originated command intents must include source spans, citations, confidence, tool permissions, and approval state.
- All God Mode actions must include impersonator, reason, duration, user-visible banner state, and immutable audit.
- All replay operations must require a new actor and reason and must never bypass policy.
- All command actions must define risk, required permissions, approval policy, replay class, and audit fields before registration.
Observability Requirements
Emit Sentry/PostHog/server log events for:
command_graph.intent_receivedcommand_graph.policy_deniedcommand_graph.approval_requestedcommand_graph.approval_decidedcommand_graph.execution_failedcommand_graph.execution_succeededcommand_graph.replay_requestedcommand_graph.replay_deniedcommand_graph.delivery_failedcommand_graph.dead_letter_created
Metrics to expose in God Mode and Integration Reliability Center:
- Command volume by action, firm, actor type, source, and status.
- Approval volume, approval latency, rejection rate, and expired approvals.
- Replay volume, replay success rate, and non-replayable denials.
- Webhook/provider delivery success, retries, latency, and dead-letter count.
- AI proposal approval rate, correction rate, cited-source coverage, cost, and latency.
Test Plan
Unit tests:
- Event and intent schemas reject malformed actor, subject, source, and idempotency data.
- Canonical input hashing is stable across object key order and date input variations.
- Policy denies cross-tenant target access.
- Policy routes high-risk actions to approval.
- Duplicate idempotency keys return the original receipt.
- Replay rejects non-replayable commands.
- Audit redaction removes secrets and auth headers.
API tests:
401unauthenticated command intent.403insufficient role or portal scope.404tenant-scoped target not found.409expected-version conflict.422schema validation failure.- Successful low-risk command writes execution, receipt, and audit.
- High-risk command creates approval and does not mutate target.
- Approved high-risk command executes and writes approval/audit records.
- Failed webhook replay preserves correlation and writes delivery attempt.
Playwright tests:
- Workflow publish requires approval where policy requires it.
- AI-generated case mutation appears in approval queue with citations and source spans.
- Webhook replay from Integration Reliability Center requires reason and records result.
- God Mode impersonation requires reason, shows banner, expires, and audits every mutation.
- Failed workflow run shows step-level command IDs and recovery actions.
Acceptance Criteria For The First Build Slice
docs/dev/casesolo-command-graph-rfc.mdandplanning/CASESOLO_COMMAND_GRAPH_RFC.mdexist and describe the target graph honestly as planned architecture.- Planning docs mark the RFC complete while leaving implementation tasks pending.
- No source behavior changes are made by this RFC task.
- Future Bootspring tasks can implement adapter manifests, trigger/action registry, webhook wizard, reliability center, workflow versioning, and AI approval against this graph contract.