---
title: Workflows (visual automation canvas, in development)
---

# Workflows (visual automation canvas, in development)

## What it is

**Workflows** is an in-development n8n/Zapier-style automation builder inside
Omniscio. A **workflow** is a trigger plus a directed acyclic
graph (DAG) of steps: deterministic actions (HTTP request, send email, send a
message, run a recipe), a branching if/else node, and AI steps. When the
trigger fires — a manual "Run" click, a cron schedule, or an incoming message
matching a filter — Omniscio walks the graph in order and runs each node, passing
data forward between steps via `{{ NodeName.path }}` template references.
Every workflow starts **off** and cannot run until a human turns it on — nothing
executes silently. (The "lands in the inbox for approval" flow this page used to
describe is NOT built: turning a workflow on is done from the canvas bar, and no
inbox card is raised.)

This page covers the visual **canvas editor** and **Build with AI** surfaces —
the sidebar row that docks a graph-editing panel beside the projects sidebar,
the same way **Flowcharts** and **Decks** do. The underlying execution engine
(trigger types, node registry, the DAG executor, the approval gate) is
documented in
[workflow-engine-contract.md](../../.claude/memory/contracts/workflow-engine-contract.md) —
read that first for anything touching how a workflow actually _runs_.

## Where to find it

Workflows ships hidden. Turn it on under **Settings → Lab → Workflows**, then open the **Workflows** row in the sidebar to dock the graph-editing panel beside the projects sidebar. The saved-workflows list itself lives in the **Pane-2 rail** — the sub-sidebar beside Omniscio's main nav — in its **Workflows** tab, beside the **Sessions** tab (AI build sessions) and the **Runs** tab (run history).

## How it behaves

### How it is gated

Workflows is an in-development feature registered as `'workflows'` in
`src/shared/unreleased-features.ts` (`settingKey: 'workflowsEnabled'`). It is
visible when any one of the following is true:

- The feature is marked `'shipped'` in the registry (releases to everyone).
- Omniscio is launched with the feature's reveal env var set.
- The user flips the **Settings → Lab → Workflows** toggle on.

No code reads `settings.workflowsEnabled` directly to decide sidebar
visibility — the gate goes through
`isUnreleasedFeatureVisibleInRenderer('workflows', …)` in
`project-visibility.ts` (`UNRELEASED_PROJECT_GATES`).

### Using the canvas (build → configure → validate → turn on)

The Phase-2 canvas editor is built. Reveal via Settings → Lab → Workflows, then
open the **Workflows** sidebar row.

The saved-workflows list lives in the **Pane-2 rail** (the sub-sidebar beside
Omniscio's main nav), not inside the panel. That placement is load-bearing rather
than cosmetic: the panel yields to the session chat while a build is open, so a
rail rendered inside it would vanish exactly when the user needs a way back to
their workflows. The rail carries three tabs — **Workflows** (the saved list),
**Sessions** (AI build sessions), and **Runs** (run history; selecting a run
shows its detail in the panel) — plus the split-view toggle. The node palette
and the React Flow canvas stay in the panel:

1. **Create** — "New workflow" makes an empty workflow and opens it.
2. **Build** — press **"Add step"** (or **double-click empty canvas**) to open the
   **step picker**: a search-first palette browsed **app-first** — your **Favorites** and
   **Recent** steps first, then one group per app (Jira, Notion, Slack…), each owning its own
   triggers and actions. Type to filter — it matches names, apps and synonyms, so "api" finds
   _HTTP request_ — arrow-key through the results and press Enter to drop the step into the middle
   of your view. A side pane previews what the highlighted step does and what it hands to later
   steps, and a **Trigger** badge marks the steps that can start a workflow. On a brand-new
   workflow the picker shows **triggers only**, under a "Choose how this workflow starts" title,
   since a workflow always begins with its trigger. You can still add a step from the **left rail**,
   which browses the same app groups (a single-step app is a chip, a multi-step app expands) and is
   the place to **drag** a step onto the canvas by hand. A **⭐** on the rail pins an app or a step
   to the top of both, and your pins and recents are remembered across restarts. A workflow needs
   one trigger, then actions / an if-branch / AI steps. Drag between node handles to connect them; a branch node has separate
   `true`/`false` output handles (labeled **Yes** / **No** on the canvas so the two paths
   are distinguishable). "Tidy up" auto-lays-out the graph (elk, lazily loaded).
   Move/delete freely; every edit is undo/redo-able and autosaves.
3. **Configure** — click a step to open the right-hand **settings drawer**. Each
   node type ships a form descriptor (its `form` on the `NodeDefinition`,
   surfaced over `WORKFLOW_NODE_TYPES`), so the drawer renders the right fields
   (text / number / select / toggle / value-ref) bound straight to the node's
   config. In a **value-ref** field, "＋ Insert a value from an earlier step"
   lists the outputs of upstream steps only (topo-bounded) and splices a
   `{{ Step.field }}` token into the field.
4. **Validate** — a banner above the canvas shows the live graph-validation
   issue count (via the shared `validateWorkflowGraph`); a workflow with no
   trigger, a cycle, an unknown type, or a forward reference is flagged.
5. **Turn it on** — the bottom bar is the workflow's on/off switch: it reads
   **Off** with a **Turn on** button, or **On** with **Turn off**. Turning it on
   flushes the pending autosave and makes the workflow live, so it reacts to its
   trigger. "Turn on" is disabled while the graph is invalid; **turning off is
   always allowed** — a live workflow whose graph later broke must still be
   stoppable. The wording is on/off but the underlying gate is unchanged
   (`approvalStatus`); there is no separate approver, because the person building
   the workflow is the only one who could approve it.

**Not yet in the canvas** (documented as gaps in the engine contract): a
run-history/observability viewer, the inbound webhook trigger (Phase 4), and
chip-rendering of `{{ }}` tokens (they show as literal text today).

### Build with AI (describe → graph)

Instead of hand-placing every node, "Build with AI" turns a plain-English
description into a whole workflow graph:

1. From the Workflows list, click **Build with AI** and describe what you want
   in a sentence or two ("every morning at 9, check my unread Gmail and send me
   a summary in Slack").
2. Omniscio creates an empty workflow, then spawns a normal Omniscio AI session (you keep
   your usual model picker) with your description pre-filled as the draft
   message — it is never auto-sent, so you can edit it first.
3. Behind the scenes the session is briefed with a hidden build primer that
   points it at the `/workflow` CLI (the same REST surface the CLI-server
   contract documents) and the live node catalogue via `GET
   /workflow/node-types` — which returns each node's `form` descriptor (fields,
   options, outputs), so the agent always builds against the real registry,
   never a hard-coded node list. The agent **builds the graph and then stops —
   it does not run the workflow** (running is a separate, human-approved,
   billable step).
4. The build session lands in the rail's **Sessions** tab, so you can watch it
   work or ask follow-up questions.
5. On desktop the canvas opens **beside that chat** and fills in live as the
   agent writes — see the next section. You no longer wait for the agent to
   finish and then go looking for the result.
6. When it's done the canvas is yours again; review the graph like any
   hand-built workflow and click **Turn on** when you're happy.

If the spawn itself fails, Omniscio deletes the orphan empty workflow it created in
step 2 rather than leaving a dead entry in your list.

### Watching a build live (the split view)

While an AI build chat is open on desktop, the workflow canvas renders
side-by-side with the chat instead of the chat replacing it, so the graph is
visibly assembled as the agent works. Four behaviours make that readable, and
each exists to answer a question the user would otherwise have to guess at:

- **The canvas is watch-only while the agent owns it** — no drag, connect,
  select, delete, palette drop, or "Tidy up". Pan and zoom deliberately stay on:
  the point is to inspect the graph being built, not stare at a frozen picture.
  The lock is **derived from live session status on every render and never
  latched**, so a crashed, errored, stalled, paused or vanished build session
  hands the canvas back by itself. A session parked on account capacity
  (`waiting`) keeps the lock — it resumes on its own, and it resumes writing.
- **Nodes the agent just touched are briefly tinted** (~1s, then it fades), so
  the eye is pointed at the change rather than hunting a re-laid-out canvas for
  it. Position is deliberately excluded from the "did this change?" comparison:
  the live arrangement is never persisted, so every push re-lays the whole graph
  out and a position-aware diff would strobe the entire canvas every time.
- **The viewport follows the growing graph** until the user pans or zooms, and
  then never takes it back. Closing the split is the reset.
- **A status strip says who owns the canvas** — "Agent is building…", or
  "Build complete — the canvas is editable again." The strip and the canvas
  read the _same_ lock value, so the strip can never promise the canvas is
  editable while it is still read-only.

**Why edits are suppressed rather than merged.** While the lock is held the
store refuses both to queue _and_ to flush a save for that workflow, so a stale
local graph can never overwrite the nodes the agent just wrote over the CLI.
There is one honest cost: the lock engages on a rising edge that can straddle
the 500 ms autosave debounce, so an edit made in the instant before a build
starts is **discarded by design** rather than persisted. In the steady state the
user is not editing; across that edge a real edit can exist and is dropped, with
the strip flipping to "Agent is building…" as the only signal.

Mobile keeps the chat-replaces-canvas behaviour (too narrow to split), and the
desktop split can be collapsed from the rail's toggle — the agent keeps building
either way.

### Where a live workflow shows up

Turning a workflow on makes it live, but the places that reflect that are spread
across three surfaces — worth knowing, because only one of them is inside
Workflows:

- **The saved-workflows list** marks each live row with a small dot (reading the
  same `approvalStatus` the canvas bar and the engine gate use), so the list can
  be scanned without opening each workflow.
- **The Cron panel**, for a `trigger.schedule` workflow: `syncWorkflowSchedule`
  creates a backing cron job named `Workflow: <name>`, and the cron sidebar does
  **not** filter by type — so it appears alongside recipes and scripts. ⚠ Nothing
  currently stops that job being disabled or deleted there, which would silently
  stop the workflow firing while its own bar still reads **On**. Two switches for
  one thing; reconciling them is an open design question.
- **The Runs tab** in the rail, once it has actually fired — each run with its
  per-step breakdown.
- **The inbox**, only if the graph ends in a node that puts something there
  (`action.create_alert`). A workflow without one runs and produces nothing the
  user would notice.

### When a workflow stops part-way and waits

A workflow does not have to finish in one go. A step can stop the whole workflow
part-way through and wait — for you to answer something, for a moment in time,
or for something to happen elsewhere. While it waits, the run shows as
**Paused** in the Runs tab, with the step it is sitting on.

Three things are worth knowing about a waiting run:

- **It survives closing the app.** A waiting run is written down, not held in
  memory. Quit Omniscio, restart your Mac, come back tomorrow — the run is still
  there, still waiting, still knows everything the earlier steps produced.
- **Carrying on does not repeat what already ran.** When the workflow picks up
  again, every step that already finished is reused as-is rather than run a
  second time. An email that was already sent is not sent again; a message that
  was already posted is not posted again. Only the step that was waiting, and
  the steps after it, actually do anything.
- **It never waits forever in silence.** A waiting step can carry a time limit.
  If nothing answers before the limit runs out and the step has a **Timeout**
  path drawn, the workflow carries on down that path; if it has none, the run is
  marked failed, saying it was waiting on a step that wasn't answered in time —
  including when the limit ran out while the app was closed, which is noticed the
  next time you open Omniscio. A step with no time limit waits indefinitely, but
  it always shows as Paused rather than looking finished.

### Asking a person to approve (the "wait for me" step)

The first step that actually pauses a run is **Wait for me** — it stops the
workflow and asks a person to approve or reject before it carries on. Add it like
any other step; it sits on the canvas with a short question, an optional bit of
detail, and up to a handful of yes/no or short-answer questions to collect along
with the decision.

- **It reaches you in the inbox.** When a run hits the step, a card appears in the
  inbox with your question and an **Approve** and a **Reject** button. Answering
  it there — or on a paired phone, the card is the same — unblocks the run. On the
  canvas the step glows amber-orange while it waits, the app's "waiting" colour.
- **Three ways out, three paths.** The step has three outgoing paths you can wire
  to different next steps: **Approved**, **Rejected**, and **Timeout**. Rejecting
  is a real answer, not an error — the workflow simply takes the Rejected path (or
  just stops, if you drew nothing there). Timeout is taken only if you set a time
  limit and nobody answers in time.
- **Its answer is usable later.** Any short-answer questions you asked are readable
  by the steps that follow, the same way one step reads another's output.
- **You cannot answer it on the workflow's behalf from a script.** The approval is
  a person's decision, so it is answered only from the inbox card (yours or your
  phone's), never from the command-line control server — an automation answering
  its own approval would defeat the point of asking.
- **Turning the workflow off or editing it cancels a pending answer.** If you turn
  the workflow off, or change its steps (which sends it back for approval), a run
  that was waiting on it will not carry on when answered — it re-checks that the
  workflow is still on and approved before spending anything, exactly as starting
  or re-running it does.

### When a step fails (retry, and an "on failure" path)

A step that fails no longer has to take the whole workflow down with it. There
are two things you can do about it, and they combine.

**Retry — try the same step again.** Open a step's settings and turn on _Retry
this step if it fails_. You choose how many attempts (up to 10) and how long to
wait between them — either the same wait each time, or a wait that doubles
(1s, 2s, 4s…), which is the polite way to treat a service that is briefly busy.
A step with retry turned off behaves exactly as it always has: one attempt.

Two deliberate limits:

- **A waiting step is never retried.** Pausing is not failing, so a step that
  stops to wait for you is left alone.
- **A step whose settings don't make sense is never retried either.** That is a
  mistake in how the step is set up, not bad luck, so trying it four more times
  would only waste time.

If a step can _send_ something (an email, a Slack message) or _create_ something
(an issue, a page), the settings panel warns you: the first attempt may already
have gone through before it reported failure, so retrying could send it twice.
Whether that is acceptable is your call — which is why it is off by default.

**An "on failure" path — carry on down a different route.** Every step except
the trigger now has a second output on its right-hand side, marked in red. Drag
a connection from there to another step, and that is where the workflow goes if
this step fails. The run carries on down that path and can still finish
successfully. The step that failed is still recorded as failed, so the Runs tab
tells you the truth about what happened — and the step you connect can read the
error text with `{{ StepName.message }}`, which is what makes "post the failure
to Slack" or "create a ticket describing what broke" possible.

If a step has **no** "on failure" connection, nothing changes: it fails, the run
is marked failed, and the steps after it are skipped.

**Retry happens first.** A step set to 3 attempts uses all three; only if all
three fail does the "on failure" path open. So the two work together — retry for
a blip, the failure path for when something is genuinely broken.

**Two things retry deliberately will not do.** An **AI step** cannot be retried
at all — a failed attempt still costs real money that the run has no way to
account for, so repeating it would quietly spend more than the run reports. The
Retry control simply is not offered on those steps. And an **HTTP step** comes
with no suggested setting: a request that times out may already have reached the
other end, so whether repeating it is safe depends on what it does — which is a
judgement only you can make. If you do turn it on for anything other than a
plain read, the panel says so.

**Not built yet:** the canvas does not yet show a live "retrying 2 of 3" label
while it happens — the run history records the outcome either way.

### Re-running a past run from a step

When a run went wrong at step 4, you usually don't want to start the whole thing over — steps 1
to 3 already did their work, and repeating them could send the same message twice. Open the run
in the Runs tab and each step has a **Replay from here** button.

What it does:

- **It starts a NEW run.** The original stays exactly as it was, so you never lose the record of
  what actually happened. The new run remembers which run it re-ran.
- **It repeats that step and only what depends on it.** Steps that don't depend on the one you
  picked are reused as-is, not repeated — so a message that already went out doesn't go out
  again just because it happened to sit on a different branch.
- **It costs money**, because the steps it re-runs really run again. It asks you to confirm
  first, and it is a desktop-only action — you cannot trigger it from your phone.
- **It uses the workflow as it was when that run started**, not as the canvas looks today.

It refuses in three cases, with a reason: the step isn't part of that run, an earlier step is
still waiting for an answer (answer it first), or the workflow has since been turned off or
un-approved — re-running is not a way around the approval gate.

### Trigger types

A workflow starts with exactly one trigger node. The engine supports six:

- **Manual** (`trigger.manual`) — fires when a user clicks Run (UI or CLI).
- **Schedule** (`trigger.schedule`) — fires on a cron schedule.
- **Message** (`trigger.message`) — fires when an incoming message (channel,
  Gmail, Pushbullet) matches a channel-scope and optional keyword filter.
- **Jira issue** (`trigger.jira_issue`) — polls every 2 minutes, fires when an
  issue is created, transitions status, or is updated. Requires Jira
  credentials.
- **Arij issue** (`trigger.arij_issue`) — real-time WebSocket subscription,
  fires on issue changes, board changes, comments, or agent-state events.
- **KMS note** (`trigger.kms_note`) — fires when a note in your KMS vault is
  created or updated and matches a tag and/or folder filter. Push-driven (no
  polling) — the KMS file watcher emits the event. Only fires on the
  **transition** from not-matching to matching (a note saved repeatedly while it
  already matches does not re-fire). A 5-minute cooldown per note prevents rapid
  flap re-fires.

**Create alert** (`action.create_alert`) is a general-purpose action node that
creates an inbox item with a title, content, and optional dedup key — useful as
the "notify me" step at the end of a KMS or any other trigger chain.

**HTTP request auth** (`action.http`) — the HTTP node can send **saved
authentication**: in the step's **Authentication** field, pick an existing
credential or add one inline (a bearer token, an API key sent as a custom
header, or basic username/password). The secret is stored encrypted in the
shared automation-credential vault; the workflow only references it by id, and
the auth header is applied at run time inside the engine port — so an author
never pastes a token into the graph, and every decrypt is audit-logged. This is
the first step of **connector convergence**: the Workflows canvas reusing the
same credential store + auth logic the v2 Automations engine already has, rather
than a parallel one. (Custom request headers are carried by the node but not yet
UI-editable — a documented fast-follow.)

**Message + page connectors** — three credential-backed action nodes ride the
same seam: **Send Slack message** (`action.slack_send_message`), **Create Notion
page** (`action.notion_create_page`), **Send Telegram message**
(`action.telegram_send_message`), and **Send Discord message**
(`action.discord_send_message`, via an incoming webhook). Each has an **account** field that picks (or
inline-adds) the saved Slack bot token / Notion integration token / Telegram bot
token; the token is resolved + applied inside the engine port at run time (never
in the graph), and every decrypt is audit-logged. The inline add-credential form
reuses the shared credentials editor, so it renders the right inputs for any
kind. (The v2 Automations actions for these providers still keep their own copy
of the send logic — migrating them onto the shared helpers is a documented
follow-up.)

### Status

The Workflow **Engine** (trigger types, node registry, DAG executor, approval
gate, IPC + CLI) shipped dark behind `workflowsEnabled`. The **Canvas editor**
(Phase 2 — build / configure / validate / request-approval, above) and
**AI-build** (Phase 3 — describe → graph, above) are both built on top of it,
also gated dark. **Pausing and carrying on** (above) is built and tested, but no
step uses it yet — the first one ships with the "wait for me" approval step. The
integration/webhook expansion (Phase 4) is not built.
Reveal via Settings → Lab → Workflows.

## For agents

### How it is mounted

Workflows is an `amc-builtin` virtual project (`__workflow__`, defined in
`src/shared/virtual-project-ids.ts`) registered once in
`src/shared/integrations/workflow.ts` and assembled into
`INTEGRATION_REGISTRY` by `npm run integrations:reindex` — mirroring how
Flowcharts (`__flowchart__`) is wired.

**Like Flowcharts**, the Workflows panel now hosts AI build sessions: `__workflow__`
is spawnable and resolver-mapped (`resolveProjectWorkDir()` maps it to
`<userData>/workflow-agent`, and it is in `SPAWNABLE_VIRTUAL_PROJECT_SENTINELS`).
It is still deliberately **not** in `SESSION_HOSTING_VIRTUAL_PROJECT_PATHS` — that
set drives things like Ctrl+T "jump to a hosting project's sessions"; Workflows
owns its own session-reveal path (the panel's Sessions tab, via
`useWorkflowsSessionHost`) instead. A workflow's own AI _step_ nodes still run
inside the engine's executor (see the engine contract), not as a user-facing
chat session — only the **build** flow below spawns a real chat session.

### Key files

| What                                               | Where                                                                                                                                                                                                                                                     |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Virtual project id                                 | `src/shared/virtual-project-ids.ts` (`WORKFLOW_PROJECT_ID`)                                                                                                                                                                                               |
| Integration manifest                               | `src/shared/integrations/workflow.ts`                                                                                                                                                                                                                     |
| Gating registry                                    | `src/shared/unreleased-features.ts` (`workflows`)                                                                                                                                                                                                         |
| Canvas editor (renderer)                           | `src/renderer/src/features/workflows/` (`WorkflowEditor`, `WorkflowCanvas`, `NodePalette`, `WorkflowListSidebar`, `NodeSettingsDrawer`, `InsertValueButton`, `ValidationBanner`, `ApprovalBar`, `workflows-store.ts`)                                     |
| Node form descriptors                              | each `src/main/services/workflow-engine/nodes/*-node.ts` (`form`) + shared types in `src/shared/workflows/types.ts`                                                                                                                                       |
| Build with AI (renderer)                           | `src/renderer/src/features/workflows/` (`BuildWithAiModal.tsx`, `useWorkflowsAiBuild.ts`, `workflows-spawn-prompt.ts`, `WorkflowSidebarTabs.tsx`, `workflows-session-host.ts`)                                                                            |
| Live build split view                              | `src/renderer/src/features/workflows/` (`WorkflowBuildPane.tsx`, `WorkflowBuildStatusStrip.tsx`, `workflows-build-lock.ts`, `useWorkflowsBuildLock.ts`, `workflow-graph-diff.ts`, `workflows-build-session.ts`) + `features/dashboard/workflows-split.ts` |
| Engine + data model + canvas + AI-build invariants | see [workflow-engine-contract.md](../../.claude/memory/contracts/workflow-engine-contract.md)                                                                                                                                                             |

## Related

Workflows is the graph-building sibling of the older automation surfaces: [Use recipes](use-recipes.md) covers single-path multi-step runs, and [Agent trigger recipes](agent-trigger-recipes.md) covers recipes an agent can fire. The credential store the HTTP, Slack, Notion, Telegram and Discord steps reuse is documented on [Automation credentials](automation-credentials.md). To have an agent walk you through building one, see [Workflow Coach](workflow-coach.md).
