Skip to content

feat(workflows): LLM workflow tool — saved multi-step agent pipelines with compiled, shareable outputs#2171

Open
benjaminshafii wants to merge 3 commits into
devfrom
feat/llm-workflows
Open

feat(workflows): LLM workflow tool — saved multi-step agent pipelines with compiled, shareable outputs#2171
benjaminshafii wants to merge 3 commits into
devfrom
feat/llm-workflows

Conversation

@benjaminshafii

@benjaminshafii benjaminshafii commented Jun 11, 2026

Copy link
Copy Markdown
Member

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:

  • Input files / context — workspace-relative paths the agent must read first (works with the shared-folder inbox for drag-in context).
  • Stored prompt steps — an ordered, reorderable list of named prompt steps.
  • Real coding agents — a run executes in a full OpenCode agent session, not a bare chat completion.
  • Compiled outputs — each run gets a run-scoped folder in the workspace outbox (.opencode/openwork/outbox/workflows/<slug>/<run>/, incl. a required run-summary.md), so results surface through the existing artifacts API: downloadable and shareable.
  • Collaboration / VCS — definitions are JSON files under .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

Run compiles 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.
  • Routes: 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)

  • New domains/workflows/workflows-view.tsx (TanStack Query + shadcn components), wired as a workspace settings tab (Workflows) next to Extensions.
  • Editor dialog (name, description, inputs, reorderable steps), run history dialog with status + "Open session", delete confirm.
  • Typed client methods on the OpenWork server client.

Tests / verification

  • bun test src/workflows.test.ts8 pass (CRUD, validation, createdAt preservation, prompt compilation, run lifecycle).
  • pnpm --filter openwork-server typecheck — clean.
  • pnpm typecheck (app) — clean.
  • Full server suite: 403 pass / 6 fail — the 6 failures are pre-existing and unrelated ('better-sqlite3' is not yet supported in Bun native-module load errors in opencode-db/runtime-config/workspace-init tests on this machine; none of those files are touched here).
  • End-to-end API smoke test against a live 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 in GET /workspace/:id/artifacts.
Compiled prompt produced during the smoke test
You are executing the workflow "Release Notes" (run 20260610-213349-b1b3cb).

## Context inputs

Read these workspace files before starting:
- notes.md

## Steps

Complete each step fully, in order, before moving to the next.

### Step 1: Summarize

Summarize notes.md

### Step 2: Step 2

Write final release notes.

## Outputs

Write every build artifact for this run into `.opencode/openwork/outbox/workflows/release-notes/20260610-213349-b1b3cb/`.
- Save the result of each step as `...step-<number>-<short-name>.md` (or an appropriate file type).
- Finish by writing `...run-summary.md` describing what was produced, decisions made, and any follow-ups.
- Do not skip writing outputs: they are the compiled results of this workflow and will be shared.

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

  • Per-step agent/model hints are stored in the schema and rendered into the compiled prompt; a per-step picker UI is a natural follow-up.
  • Auto-completing runs from session events and a one-click "share run outputs" bundle are obvious next steps.

Update 2: co-editing + Daytona e2e frame proof

Co-editing (commit fe5d07c6e)

Workflows are now collaboration-aware:

  • Live sync — workflow and run lists poll every 3s, so collaborators' saves (or git pull) appear on every connected client within seconds. Header shows a "Synced live" indicator.
  • Optimistic concurrency — upserts carry a baseUpdatedAt token; the server rejects stale writes with 409 workflow_conflict so nobody silently overwrites a teammate. Covered by a bun test.
  • Proactive remote-update banner — if a collaborator saves while your editor is open, a banner appears before you hit save, with one-click Load latest.
  • Conflict recovery — a stale save shows a conflict banner, disables Save, and Load latest pulls the collaborator's version into the editor.
  • Last-run status on each card (badge + relative time + "Last session" shortcut) via new GET /workspace/:id/workflow-runs.
  • "Start from an example" empty state prefills a 3-step research-digest workflow.

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

Daytona proxy URLs are not permanent. If the link is down, restart sandbox openwork-test-20260611-064717, re-serve /daytona-artifacts on port 8090, and regenerate via daytona preview-url. Frames persist on the openwork-eval-artifacts volume at /daytona-artifacts/proof-frames-workflows/.

Validated flow (branch feat/llm-workflows @ 07c31d755, sandbox openwork-test-20260611-064717):

  1. Welcome → create local workspace /workspace/hello through the UI
  2. Settings → Workflows tab (new workspace settings entry)
  3. Empty state → Start from an example → editor prefilled → Save (verified JSON persisted at .opencode/openwork/workflows/weekly-research-digest.json)
  4. Run → run record created → new agent session opens with the compiled prompt (inputs, steps, output contract) in the composer
  5. Card shows Running badge + "Last run 1m ago" + Last session shortcut
  6. Co-edit live sync: collaborator edit written to the workflow JSON on disk → card updates to "edited by collaborator Bob", 4 steps, "edited just now" with no user action (≤3s)
  7. Remote-update banner: collaborator saves while the editor is open → warning banner + Load latest (CDP-asserted via data-testid="workflow-remote-update-banner")
  8. 409 conflict: stale Save rejected — "Your changes were not saved", Save disabled, collaborator version preserved; Load latest recovers it (asserted: description shows Bob's text, save re-enabled)
  9. Run history dialog: statuses, relative times, run-scoped output dirs, Open session links

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.ts9 pass (incl. new workflow_conflict co-edit test)
  • pnpm --filter openwork-server typecheck — clean
  • pnpm typecheck (app) — clean

…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.
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview, Comment Jun 11, 2026 2:04pm
openwork-den Ready Ready Preview, Comment Jun 11, 2026 2:04pm
openwork-den-worker-proxy Ready Ready Preview, Comment Jun 11, 2026 2:04pm
openwork-landing Ready Ready Preview, Comment, Open in v0 Jun 11, 2026 2:04pm

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ? (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
: `Edited ${formatRelativeTime(workflow.updatedAt)} · never run`}
: `Edited ${formatRelativeTime(workflow.updatedAt)}`}

}

function allRunsQueryKey(workspaceId: string | null) {
return ["openwork", "workflow-runs", workspaceId, "all"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant