# Let an agent propose an automation

Asked to automate something, an agent can propose the whole workflow: what starts it, the steps, and the conditions that decide which steps run. A person should read it, change what's wrong, and turn it on themselves. Until they save, nothing runs.

Have the agent propose through a tool whose input is a `WorkflowDefinition`: a trigger, and nodes that are steps or conditions. Check the input's shape on your server, store it as the proposal with its agent, and send it to the page. The agent picks from your lists (the events that can start a workflow, the actions a step can take, the fields a condition can test), so check those ids too: the editor shows an id it doesn't know as the id, and your checks should say it doesn't exist.

WorkflowEditor shows the proposal as the agent's, by the name your app gives it, and marks everything the person changes against it: added, changed, moved, and what they removed. Conditions are built from your fields as FilterBar's rules are, a schedule is `schedule-editor`'s shape, and Undo and Back to the proposal take changes back. The list is what a screen reader reads and works from the keyboard; the graph view lays the same nodes out, with Then and Else on the arrows.

Your rules go in one function that runs in two places: as `validate` while the person edits, so each problem shows on its node and errors lock Save with the reason, and on your server before it stores anything. Save calls `onSave(definition, hash)`. Your server recomputes the hash from the definition it received, checks the rules again, and stores both. What runs is read from what was saved, by its hash, never from the agent's proposal.

Web page: https://candor.themethodic.com/patterns/agent-proposed-automation

## Components

- **Workflow editor** (`workflow-editor`, Pro; https://candor.themethodic.com/docs/workflow-editor.md): The agent's trigger, steps and conditions, edited by a person with every change marked, saved with its change hash (Pro).
- **Change hash** (`change-hash`, Pro; https://candor.themethodic.com/docs/change-hash.md): The same sha256 of canonical JSON in the browser and on the server, so the saved definition is exactly the one the person saw.
- **Filter bar** (`filter-bar`, Pro; https://candor.themethodic.com/docs/filter-bar.md): The fields, conditions and values a condition's terms are built from; `useFilterChips` words them anywhere else (Pro).
- **Schedule editor** (`schedule-editor`, Pro; https://candor.themethodic.com/docs/schedule-editor.md): The full editor for a scheduled job, whose `ScheduleRecurrence` a schedule trigger uses (Pro).
- **Graph view** (`graph-view`, Pro; https://candor.themethodic.com/docs/graph-view.md): The graph the editor's graph view draws; on its own, a read-only view of a saved workflow (Pro).
- **Locked** (`locked`, free; https://candor.themethodic.com/docs/locked.md): Save says why it can't be used: a problem to fix, a save in progress, or a person who can't save.

## Install

```bash
npx candor-ui add workflow-editor change-hash
```

## Code

`lib/workflow-checks.ts`: Your rules, in one function the editor runs as `validate` and the server runs before it saves.

```tsx
import type { WorkflowDefinition, WorkflowIssue, WorkflowNode } from "@/components/candor/workflow-editor"

/** Your app's lists: what can start a workflow, what a step can do, and what a condition can test. */
export const EVENTS = ["invoice.received", "invoice.disputed"]
export const ACTIONS = ["request-approval", "mark-ready", "pay", "notify", "hold"]
export const FIELDS = ["amount", "supplier", "reference"]

/**
 * The rules your app holds a workflow to, run in the browser as the person edits (WorkflowEditor's `validate`) and
 * again on the server before it saves. Each issue names the node it is about, so the editor shows it there.
 */
export function checkWorkflow(definition: WorkflowDefinition): WorkflowIssue[] {
  const issues: WorkflowIssue[] = []
  if (definition.trigger.kind === "event" && definition.trigger.event !== null && !EVENTS.includes(definition.trigger.event))
    issues.push({ trigger: true, message: "This event doesn't exist in the app. Choose another." })
  // A payment needs an approval before it on every path that reaches it.
  const walk = (nodes: WorkflowNode[], approved: boolean): boolean => {
    let ok = approved
    for (const node of nodes) {
      if (node.kind === "step") {
        if (node.action !== null && !ACTIONS.includes(node.action)) issues.push({ node: node.id, message: "This action doesn't exist in the app. Choose another." })
        if (node.action === "request-approval") ok = true
        if (node.action === "pay" && !ok) issues.push({ node: node.id, message: "A payment needs an approval step before it on the same path." })
      } else {
        for (const rule of node.rules) if (!FIELDS.includes(rule.field)) issues.push({ node: node.id, message: `There's no field called ${rule.field}.` })
        const a = walk(node.whenTrue, ok)
        const b = walk(node.whenFalse, ok)
        ok = a && b
      }
    }
    return ok
  }
  walk(definition.nodes, false)
  return issues
}

```

`app/api/workflows/[id]/route.ts`: The route that saves an edited workflow: the hash checked, the rules run again, and the definition stored with who saved it.

```tsx
import { changeHash } from "@/lib/candor/change-hash"
import type { WorkflowDefinition } from "@/components/candor/workflow-editor"
import { checkWorkflow } from "@/lib/workflow-checks"
import { db } from "@/lib/db" // yours: proposals, saved workflows, the audit log
import { viewerOf } from "@/lib/auth" // yours: who is asking

/** Save a workflow a person edited from an agent's proposal. The body is `{ definition, hash }`. */
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const viewer = await viewerOf(request)
  if (!viewer) return Response.json({ error: "Sign in to save workflows." }, { status: 401 })
  // Check the shape with zod (or your own schema) before trusting it; this sketch assumes it's a WorkflowDefinition.
  const { definition, hash } = (await request.json()) as { definition: WorkflowDefinition; hash: string }

  // The same canonical JSON as the browser's, so the hash names exactly the definition that arrived.
  if ((await changeHash(definition)) !== hash) return Response.json({ error: "The workflow doesn't match its hash. Reload and save again." }, { status: 409 })

  // Your rules again, on the server: the browser's check is for the person, this one is the gate.
  const issues = checkWorkflow(definition)
  if (issues.some((i) => i.severity !== "warning")) return Response.json({ issues }, { status: 422 })

  const proposal = await db.workflowProposal(id)
  if (!proposal) return Response.json({ error: "There's no such proposal." }, { status: 404 })
  const at = new Date().toISOString()
  // Store the definition with its hash and who saved it. What runs is read from here by its hash, never from the proposal.
  await db.saveWorkflow({ proposalId: id, definition, hash, savedBy: viewer.id, at, agent: proposal.agentId })
  await db.audit({ at, who: viewer.id, what: `Saved workflow ${id} (sha256:${hash}), proposed by ${proposal.agentName}.` })
  return Response.json({ saved: true })
}

```

`edit-workflow.tsx`: The page's side: WorkflowEditor with your lists and rules, and Save sending the definition and its hash.

```tsx
"use client"

import * as React from "react"
import { WorkflowEditor, type WorkflowDefinition, type WorkflowIssue } from "@/components/candor/workflow-editor"
import { checkWorkflow } from "@/lib/workflow-checks"
import { actions, events, fields } from "@/lib/workflow-lists" // yours: the labels for the ids in workflow-checks

export function EditWorkflow({ id, proposal, agentName }: { id: string; proposal: WorkflowDefinition; agentName: string }) {
  const [failure, setFailure] = React.useState<string>()
  return (
    <WorkflowEditor
      title="New automation"
      agent={agentName}
      proposal={proposal}
      events={events}
      actions={actions}
      fields={fields}
      validate={checkWorkflow}
      error={failure}
      onSave={async (definition, hash) => {
        setFailure(undefined)
        let res: Response
        try {
          res = await fetch(`/api/workflows/${id}`, {
            method: "PUT",
            headers: { "content-type": "application/json" },
            body: JSON.stringify({ definition, hash }),
          })
        } catch {
          setFailure("No answer from the server, so it isn't known whether the workflow was saved. Reload to see.")
          throw new Error("no answer")
        }
        const body = (await res.json()) as { issues?: WorkflowIssue[]; error?: string }
        // The server's own problems go back to the editor, which shows each on its node.
        if (res.status === 422 && body.issues) return body.issues
        if (!res.ok) {
          setFailure(body.error ?? "The workflow wasn't saved.")
          throw new Error(body.error)
        }
      }}
    />
  )
}

```

## Notes

- The editor never saves or runs anything itself. Nothing reaches your server until the person presses Save, and nothing should run until your server has stored the definition.
- A step's note is shown as text, never as HTML or a link, since the agent may have written it.
- If the agent sends a new proposal while the person is editing, their draft stays as it is and the editor offers to start from the new one. Without edits, the new proposal is taken.
- Problems your server finds can come back from `onSave` as issues: each shows on its node, marked as from the last save, and nothing is marked saved.
