feat(workflows): LLM workflow tool — saved multi-step agent pipelines with compiled, shareable outputs#2171
feat(workflows): LLM workflow tool — saved multi-step agent pipelines with compiled, shareable outputs#2171benjaminshafii wants to merge 3 commits into
Conversation
…ines with compiled outputs Adds a Workflows feature to OpenWork: repeatable, multi-step agent runs stored inside the workspace. Server (openwork-server): - New workflows module: workflow definitions as JSON under .opencode/openwork/workflows/<slug>.json (git-versioned with the project), run records under .opencode/openwork/workflows/runs/. - Workflows declare input files (context), ordered prompt steps, and a run-scoped output dir in the workspace outbox so results surface as shareable artifacts via the existing artifacts API. - Runs compile a workflow into a single agent prompt (inputs, steps, output contract incl. run-summary.md). - REST: GET/POST/DELETE /workspace/:id/workflows[/:slug], GET/POST /workspace/:id/workflows/:slug/runs, PATCH .../runs/:runId, with write-scope checks, approvals, and audit entries. - workflows capability advertised in /capabilities. - bun tests for CRUD, validation, prompt compilation, run lifecycle. App (renderer): - New Workflows workspace settings tab (sidebar group), with list, create/edit dialog (inputs + reorderable steps), run history dialog, and delete confirm. - Running a workflow creates a run record, opens a new agent session seeded with the compiled prompt, links the session to the run, and navigates to it; run history deep-links back to sessions. - Typed OpenWork server client methods for all new endpoints.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
3 issues found across 10 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/server/src/workflows.ts">
<violation number="1" location="apps/server/src/workflows.ts:354">
P1: `runId` is not path-safe validated before filesystem access, enabling path traversal outside the workflow-runs directory.</violation>
</file>
<file name="apps/app/src/react-app/shell/settings-route.tsx">
<violation number="1" location="apps/app/src/react-app/shell/settings-route.tsx:2188">
P2: Workflow write access is defaulted to true, so write actions are enabled even when workflow write capability is missing/unknown.</violation>
</file>
<file name="apps/app/src/react-app/domains/workflows/workflows-view.tsx">
<violation number="1" location="apps/app/src/react-app/domains/workflows/workflows-view.tsx:615">
P2: Run history errors are hidden as an empty state because the runs dialog has no `runsQuery.isError` branch.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| runId: string, | ||
| payload: UpdateWorkflowRunPayload, | ||
| ): Promise<WorkflowRunRecord> { | ||
| const id = runId.trim(); |
There was a problem hiding this comment.
P1: runId is not path-safe validated before filesystem access, enabling path traversal outside the workflow-runs directory.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/workflows.ts, line 354:
<comment>`runId` is not path-safe validated before filesystem access, enabling path traversal outside the workflow-runs directory.</comment>
<file context>
@@ -0,0 +1,404 @@
+ runId: string,
+ payload: UpdateWorkflowRunPayload,
+): Promise<WorkflowRunRecord> {
+ const id = runId.trim();
+ if (!id) {
+ throw new ApiError(400, "invalid_workflow_run", "Workflow run id is required");
</file context>
| client={selectedWorkspaceEndpoint?.client ?? openworkClient} | ||
| workspaceId={runtimeWorkspaceId} | ||
| busy={busy} | ||
| canWrite={routeOpenworkCapabilities?.workflows?.write ?? true} |
There was a problem hiding this comment.
P2: Workflow write access is defaulted to true, so write actions are enabled even when workflow write capability is missing/unknown.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/src/react-app/shell/settings-route.tsx, line 2188:
<comment>Workflow write access is defaulted to true, so write actions are enabled even when workflow write capability is missing/unknown.</comment>
<file context>
@@ -2176,6 +2179,37 @@ function SettingsRouteContent(props: SettingsSurfaceProps = {}) {
+ client={selectedWorkspaceEndpoint?.client ?? openworkClient}
+ workspaceId={runtimeWorkspaceId}
+ busy={busy}
+ canWrite={routeOpenworkCapabilities?.workflows?.write ?? true}
+ onLaunchRun={async ({ prompt }) => {
+ if (!opencodeClient || !selectedWorkspaceId) return null;
</file context>
| <div className="min-h-0 flex-1 space-y-2 overflow-y-auto"> | ||
| {runsQuery.isLoading ? ( | ||
| <div className="text-[13px] text-dls-secondary">Loading runs...</div> | ||
| ) : runs.length === 0 ? ( |
There was a problem hiding this comment.
P2: Run history errors are hidden as an empty state because the runs dialog has no runsQuery.isError branch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/src/react-app/domains/workflows/workflows-view.tsx, line 615:
<comment>Run history errors are hidden as an empty state because the runs dialog has no `runsQuery.isError` branch.</comment>
<file context>
@@ -0,0 +1,690 @@
+ <div className="min-h-0 flex-1 space-y-2 overflow-y-auto">
+ {runsQuery.isLoading ? (
+ <div className="text-[13px] text-dls-secondary">Loading runs...</div>
+ ) : runs.length === 0 ? (
+ <div className="rounded-xl border border-dashed border-dls-border px-4 py-6 text-[13px] text-dls-secondary">
+ No runs yet. Hit Run to execute this workflow with an agent.
</file context>
…, last-run status - Live sync: workflows and run lists poll every 3s so collaborators' edits (and git pulls) appear in near real time; 'Synced live' state shown in the header. - Co-editing safety: upserts carry a baseUpdatedAt token; the server rejects stale writes with 409 workflow_conflict so nobody silently overwrites a collaborator. The editor shows a conflict banner with one-click 'Load latest'. - Proactive remote-update banner: while editing, if a collaborator saves the same workflow, a banner appears before you even hit save. - Last-run status chip + relative timestamps on workflow cards, with a 'Last session' shortcut; run history shows relative times. - Empty state offers a one-click 'Start from an example' prefilled three-step research-digest workflow. - New GET /workspace/:id/workflow-runs endpoint for cross-workflow run status; conflict path covered by a new bun test.
There was a problem hiding this comment.
3 issues found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/server/src/workflows.ts">
<violation number="1" location="apps/server/src/workflows.ts:225">
P2: The new optimistic-concurrency check is vulnerable to a read-check-write race, so concurrent edits can still overwrite each other silently.</violation>
</file>
<file name="apps/app/src/react-app/domains/workflows/workflows-view.tsx">
<violation number="1" location="apps/app/src/react-app/domains/workflows/workflows-view.tsx:137">
P2: Query key collision: workflow slug "all" conflicts with the global runs cache key, causing incorrect run data to be shared between queries.</violation>
<violation number="2" location="apps/app/src/react-app/domains/workflows/workflows-view.tsx:535">
P2: The new "never run" label can be false because it is derived from a globally truncated runs list (max 100).</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| : undefined; | ||
|
|
||
| const existing = readWorkflowRecord(await readJsonFile<unknown>(workflowPath(workspaceRoot, slug))); | ||
| if ( |
There was a problem hiding this comment.
P2: The new optimistic-concurrency check is vulnerable to a read-check-write race, so concurrent edits can still overwrite each other silently.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/workflows.ts, line 225:
<comment>The new optimistic-concurrency check is vulnerable to a read-check-write race, so concurrent edits can still overwrite each other silently.</comment>
<file context>
@@ -215,6 +222,18 @@ export async function upsertWorkflow(
: undefined;
const existing = readWorkflowRecord(await readJsonFile<unknown>(workflowPath(workspaceRoot, slug)));
+ if (
+ existing &&
+ typeof payload.baseUpdatedAt === "number" &&
</file context>
| <span className="text-[11px] text-dls-secondary"> | ||
| {latestRun | ||
| ? `Last run ${formatRelativeTime(latestRun.createdAt)} · edited ${formatRelativeTime(workflow.updatedAt)}` | ||
| : `Edited ${formatRelativeTime(workflow.updatedAt)} · never run`} |
There was a problem hiding this comment.
P2: The new "never run" label can be false because it is derived from a globally truncated runs list (max 100).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/src/react-app/domains/workflows/workflows-view.tsx, line 535:
<comment>The new "never run" label can be false because it is derived from a globally truncated runs list (max 100).</comment>
<file context>
@@ -341,102 +470,137 @@ export function WorkflowsView(props: WorkflowsViewProps) {
+ <span className="text-[11px] text-dls-secondary">
+ {latestRun
+ ? `Last run ${formatRelativeTime(latestRun.createdAt)} · edited ${formatRelativeTime(workflow.updatedAt)}`
+ : `Edited ${formatRelativeTime(workflow.updatedAt)} · never run`}
+ </span>
+ <div className="flex flex-wrap gap-2">
</file context>
| : `Edited ${formatRelativeTime(workflow.updatedAt)} · never run`} | |
| : `Edited ${formatRelativeTime(workflow.updatedAt)}`} |
| } | ||
|
|
||
| function allRunsQueryKey(workspaceId: string | null) { | ||
| return ["openwork", "workflow-runs", workspaceId, "all"]; |
There was a problem hiding this comment.
P2: Query key collision: workflow slug "all" conflicts with the global runs cache key, causing incorrect run data to be shared between queries.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/src/react-app/domains/workflows/workflows-view.tsx, line 137:
<comment>Query key collision: workflow slug "all" conflicts with the global runs cache key, causing incorrect run data to be shared between queries.</comment>
<file context>
@@ -91,13 +125,18 @@ function editorStateFromWorkflow(workflow: OpenworkWorkflowItem): EditorState {
}
+function allRunsQueryKey(workspaceId: string | null) {
+ return ["openwork", "workflow-runs", workspaceId, "all"];
+}
+
</file context>
| return ["openwork", "workflow-runs", workspaceId, "all"]; | |
| return ["openwork", "workflow-runs-all", workspaceId]; |
The live composer reads its draft from the in-memory composer state store keyed by session id; the localStorage draft store alone does not hydrate it. Seed both so the compiled run prompt appears in the composer immediately after Run navigates to the new session.
What
Adds Workflows to OpenWork: a lightweight LLM workflow tool for the recurring "I have all this stuff I want to process in an iterated way, and the build artifacts matter" pattern (GNU Autotools x Notion).
A workflow is a saved pipeline that lives inside the workspace:
.opencode/openwork/outbox/workflows/<slug>/<run>/, incl. a requiredrun-summary.md), so results surface through the existing artifacts API: downloadable and shareable..opencode/openwork/workflows/, so they're versioned with the project in git and visible to everyone connected to the same OpenWork server.How it works
Runcompiles the workflow (inputs + steps + output contract) into a single agent prompt, creates a run record, opens a new session seeded with the compiled prompt for one-keystroke launch, and links the session to the run. Run history deep-links back to the session; outputs land in the outbox as artifacts.Changes
Server (
apps/server)src/workflows.ts: definitions, validation (kebab slugs, workspace-relative inputs only, step caps), prompt compiler, run records.GET/POST/DELETE /workspace/:id/workflows[/:slug],GET/POST /workspace/:id/workflows/:slug/runs,PATCH .../runs/:runId— with collaborator scope, approvals, and audit entries like skills/commands.workflows: { read, write }advertised in capabilities.App (
apps/app)domains/workflows/workflows-view.tsx(TanStack Query + shadcn components), wired as a workspace settings tab (Workflows) next to Extensions.Tests / verification
bun test src/workflows.test.ts— 8 pass (CRUD, validation, createdAt preservation, prompt compilation, run lifecycle).pnpm --filter openwork-server typecheck— clean.pnpm typecheck(app) — clean.'better-sqlite3' is not yet supported in Bunnative-module load errors inopencode-db/runtime-config/workspace-inittests on this machine; none of those files are touched here).openwork-server(--approval auto, temp workspace): create → get → list → start run (compiled prompt verified, see below) → patch run status/session → list runs → invalid status & path-escape inputs rejected → delete; also verified run outputs written to the run output dir appear inGET /workspace/:id/artifacts.Compiled prompt produced during the smoke test
I could not capture a desktop video in this environment — to reproduce the UI flow:
pnpm dev, open a workspace → Settings → Workflows → New workflow → add a step → Run (lands in a new session with the compiled prompt drafted; send to execute).Notes / follow-ups
Update 2: co-editing + Daytona e2e frame proof
Co-editing (commit
fe5d07c6e)Workflows are now collaboration-aware:
git pull) appear on every connected client within seconds. Header shows a "Synced live" indicator.baseUpdatedAttoken; the server rejects stale writes with 409workflow_conflictso nobody silently overwrites a teammate. Covered by a bun test.GET /workspace/:id/workflow-runs.Also fixed (commit
07c31d755): Run now hydrates the in-memory composer store, so the compiled prompt reliably appears in the new session's composer.E2E validation on Daytona (real Electron, driven via CDP)
Frame-by-frame HTML proof (11 named frames, inspected before sharing):
https://8090-eycuunhigfajxagb.daytonaproxy01.net/proof-frames-workflows/index.html
Validated flow (branch
feat/llm-workflows@07c31d755, sandboxopenwork-test-20260611-064717):/workspace/hellothrough the UI.opencode/openwork/workflows/weekly-research-digest.json)data-testid="workflow-remote-update-banner")Setup notes (labeled per evidence standard): settings navigation used direct hash routing (no Settings button exposed in the Daytona dev shell outside settings); collaborator edits simulated by writing the workflow JSON file in the workspace — the same path a second connected client or a git pull takes.
Tests re-run for this update
bun test src/workflows.test.ts— 9 pass (incl. newworkflow_conflictco-edit test)pnpm --filter openwork-server typecheck— cleanpnpm typecheck(app) — clean