workflows.ts127 lines · main
1import { brivenError } from '@briven/shared';
2
3/**
4 * Workflows / durable execution — Phase 4 future-feature SKELETON.
5 *
6 * Like `branches.ts`, this file exists to pin the data model + API
7 * shape today so the dashboard and CLI can wire against it, and the
8 * full implementation lands incrementally without breaking consumers.
9 *
10 * The full implementation has four parts:
11 *
12 * 1. Meta-DB tables: `workflow_definitions` (versioned),
13 * `workflow_runs` (per-execution state), `workflow_steps` (per-
14 * step state with deterministic-replay seed). All cascade-delete
15 * with their parent project.
16 *
17 * 2. A `workflow()` wrapper in `@briven/cli/server` next to query /
18 * mutation / action. Authors declare a workflow as a function
19 * with explicit checkpoints — each `await ctx.step(...)` records
20 * its outcome so a crash mid-flight resumes from the last step
21 * instead of replaying side effects.
22 *
23 * 3. A worker process (`apps/workflow-worker` or as a mode on the
24 * runtime) that picks up `pending` runs from the queue, executes
25 * each step in a deno isolate, persists step outcomes, retries
26 * transient failures with exponential backoff.
27 *
28 * 4. Dashboard surface: list runs, step-by-step state inspector,
29 * manual retry / cancel buttons, audit-log integration.
30 *
31 * Today the code below is the type contract + a no-op stub. Calls
32 * throw `not_implemented` so the dashboard renders an "available in
33 * v2" banner instead of breaking.
34 *
35 * Design intent worth pinning before v1 ships:
36 *
37 * - Workflows are deterministic-replay, not event-sourced. Each
38 * `await ctx.step(name, fn)` records `(stepName, outcome)`. On
39 * resume the worker replays the workflow body and short-circuits
40 * each step that already has a recorded outcome.
41 *
42 * - External-system side effects (http calls, polar charges, email
43 * sends) MUST live inside a step — never inline in the workflow
44 * body. The worker hard-rejects a run that side-effects outside
45 * a step.
46 *
47 * - The wire format for step outcomes is JSON + 1MB cap. Outcomes
48 * beyond that get an opaque id; the actual payload lives in
49 * object storage.
50 */
51
52export interface WorkflowDefinition {
53 readonly id: string;
54 readonly projectId: string;
55 readonly name: string;
56 readonly version: number;
57 readonly bundle: string; // path to the compiled workflow function
58 readonly createdAt: Date;
59}
60
61export type WorkflowRunStatus =
62 | 'pending'
63 | 'running'
64 | 'succeeded'
65 | 'failed'
66 | 'cancelled';
67
68export interface WorkflowRun {
69 readonly id: string;
70 readonly workflowId: string;
71 readonly projectId: string;
72 readonly status: WorkflowRunStatus;
73 readonly input: unknown;
74 readonly result: unknown;
75 readonly error: { code: string; message: string } | null;
76 readonly startedAt: Date;
77 readonly finishedAt: Date | null;
78 /** Step outcomes by name. Empty until the worker runs steps. */
79 readonly steps: Readonly<Record<string, WorkflowStepOutcome>>;
80}
81
82export interface WorkflowStepOutcome {
83 readonly status: 'pending' | 'running' | 'succeeded' | 'failed';
84 readonly attempt: number;
85 readonly startedAt: string;
86 readonly finishedAt: string | null;
87 readonly result: unknown;
88 readonly error: { code: string; message: string } | null;
89}
90
91export interface TriggerWorkflowInput {
92 readonly projectId: string;
93 readonly workflowName: string;
94 readonly input: unknown;
95}
96
97export async function triggerWorkflow(_input: TriggerWorkflowInput): Promise<WorkflowRun> {
98 throw new brivenError(
99 'not_implemented',
100 'workflows are a Phase 4 feature — see docs/BUILD_PLAN.md',
101 { status: 501 },
102 );
103}
104
105export async function listWorkflowRuns(
106 _projectId: string,
107 _options?: { workflowName?: string; status?: WorkflowRunStatus; limit?: number },
108): Promise<readonly WorkflowRun[]> {
109 // Returns empty until the worker ships. Dashboard renders the
110 // "no runs yet" empty state, which is accurate.
111 return [];
112}
113
114export async function getWorkflowRun(
115 _projectId: string,
116 _runId: string,
117): Promise<WorkflowRun | null> {
118 return null;
119}
120
121export async function cancelWorkflowRun(_projectId: string, _runId: string): Promise<void> {
122 throw new brivenError(
123 'not_implemented',
124 'workflows are a Phase 4 feature — see docs/BUILD_PLAN.md',
125 { status: 501 },
126 );
127}