{"slug":"procedure-builder","category":"agents","job":"steer","added":"2026-10-18T20:45Z","title":"Procedure Builder","description":"A doc-style editor for an agent's step-by-step procedure — a / step picker, IF / ELSE branches with plain-language or code conditions, section jumps, and escalate / switch actions.","url":"https://ai-patterns.app/patterns/agents/procedure-builder","markdown":"https://ai-patterns.app/patterns/agents/procedure-builder.md","json":"https://ai-patterns.app/api/patterns/agents/procedure-builder","seenIn":[{"team":"Intercom","source":"https://www.intercom.com/help/en/articles/13449439-building-fin-procedures"}],"spec":"# Procedure Builder\n\n## Summary\nA document-style editor for writing the step-by-step procedure an AI agent follows — numbered instructions in plain language, IF / ELSE branches, section headers, jumps between sections, and terminal actions like Escalate or Switch procedure. Steps are added from a \"/\" picker, so authoring feels like writing a doc rather than wiring a flowchart, while the result is still structured enough for the agent to execute deterministically where it matters.\n\n## When to use\n- Configuring how an agent handles a multi-step support or operations task (refunds, damage claims, account changes) where some steps are judgment calls and some are hard rules.\n- When the people writing the procedure are domain experts, not engineers — plain-language instructions and conditions are the default, code is opt-in.\n- When branching is shallow (one level of IF / ELSE) and the procedure reads top to bottom.\n\n## When not to use\n- Deeply nested or parallel logic, loops, or many-way merges — use a canvas-based Flowchart, where the shape of the graph is the point.\n- A single prompt or persona with no ordered steps — a plain instructions field is enough.\n- Automations with no AI judgment at all (every condition is a field comparison) — a rules or workflow builder is more honest about what runs.\n\n## Anatomy\n- A numbered list of steps (1, 2, 3…). Section headers are unnumbered and break the list into named parts.\n- Instruction step: free text describing what the agent should do. Can contain inline reference chips — a bold verb plus a target (\"**Update** Resolution method\", \"**Tag** Refund initiated\", \"**Use** Order replacement\", \"**Read** ETA\") — for the data, tools, and tags the instruction touches.\n- Condition step:\n  - An \"IF\" header with a violet branch badge and a ⋮ menu (add/remove ELSE, switch between plain language and code, delete).\n  - A bordered condition field — prose by default, or a syntax-highlighted expression in code mode, toggled with a `<>` button in its corner.\n  - A lettered sub-list (A, B, C…) of steps to run when the condition holds, hung off a vertical rail that ends in a \"+\" button.\n  - An optional \"ELSE\" header with the same rail, sub-list, and \"+\" button.\n- Go to step: a blue badge, \"Go to\", and a picker of the procedure's section headers.\n- Action steps: an orange badge marks steps that hand control elsewhere — \"Escalate to [team]\", \"Switch to [procedure]\", \"Run [workflow]\", \"End procedure\".\n- The step picker: a \"/Search steps…\" input with a menu grouped into structure (Instruction, Condition, Section header, Go to) and, below a divider, actions (Escalate, Switch, Run workflow, End).\n\n## Behavior\n- \"Add step\" at the bottom of the procedure, or the \"+\" at the end of a branch rail, opens the step picker at that position with focus in the search input. Only one picker is open at a time.\n- Typing filters the menu; Arrow keys move the highlight, Enter or a click inserts that step type in place of the picker. Escape, Backspace on an empty search, or moving focus away removes the picker without adding anything.\n- Inside a branch the picker offers a reduced menu — no Condition or Section header — because branches are one level deep and sections belong to the top level. Labels get more explicit there (\"Go to section\", \"End procedure\").\n- Choosing Condition inserts the IF block with focus in the condition field and one empty sub-step whose placeholder explains its role: \"Describe what the agent should do when this condition is met.\"\n- Choosing Go to, Escalate, Switch, or Run workflow opens its target picker immediately, since the step is incomplete without one.\n- In an instruction, Enter opens the step picker on the next line, Shift+Enter adds a line break, Backspace on an empty instruction deletes it, and \"/\" in an empty instruction swaps it for the picker — the same muscle memory as block-based docs.\n- Go to targets are bound to section headers by identity, not text: renaming a section updates every step that points at it, and deleting a section clears those targets rather than leaving a dangling jump.\n- Numbering is derived from position and updates live as steps are added, removed, or branched.\n- New steps fade and slide in a few pixels; removed steps fade out and the rest of the list glides into place. Menus scale in from their anchor. Motion is short (≤200 ms) so it never delays typing.\n- Every non-condition step reveals a remove button on hover or focus; conditions are removed from their ⋮ menu.\n\n## Content guidelines\n- Write instructions as direct imperatives to the agent: \"Explain that a reorder can't be placed due to policy,\" not \"The agent should maybe explain…\".\n- Plain-language conditions should name the observable evidence and give examples (\"the image clearly shows spilled or leaking packaging, e.g. sauce, soup, drinks\"). Vague conditions produce inconsistent branching.\n- Reach for code conditions only for hard thresholds — dates, totals, counts — that must never be left to judgment. Use dotted, human-readable field paths (`order.total`, `policy.auto_approve_limit`).\n- Name sections by the goal of that part of the procedure (\"Verify the claim\", \"Offer a resolution\") so Go to steps read as sentences.\n- End every branch with a clear outcome — a resolution, a Switch, an Escalate, or End — so the agent never falls off the end of a branch ambiguously.\n\n## Accessibility\n- The procedure is an ordered list (`<ol>`) and each branch is a nested ordered list, so screen readers announce position and nesting; visual markers (\"3.\", \"A.\") are `aria-hidden` duplicates of that structure.\n- The step picker input is a `combobox` with `aria-expanded`, `aria-controls`, and `aria-activedescendant` pointing into a `listbox` of `option`s; the highlight follows the keyboard, not just the pointer.\n- Target pickers are buttons with `aria-haspopup=\"listbox\"`; the list takes focus on open, supports Arrow keys / Enter / Escape, and returns focus to the button on close.\n- The ⋮ menus are `aria-haspopup=\"menu\"` buttons with `role=\"menu\"` / `menuitem` items, roving focus with Arrow keys, Home / End, and Escape to close.\n- Every field has an accessible name even though its visible label is a placeholder; the code toggle is a toggle button with `aria-pressed`. Hover-only controls (remove, code toggle) also appear on keyboard focus.\n- Syntax highlighting in code mode is a decorative overlay; the underlying textarea holds the real text, so it remains editable and readable by assistive tech.\n\n## Related patterns\n- **Flowchart** — Use it instead when the shape of the logic matters more than its wording and a canvas reads better than a doc.\n- **Create Agent** — Use it as well to set up the agent the procedure belongs to.\n- **Agent Triggers** — Use it as well to decide when the agent runs, while the procedure decides what it does once running.\n- **Command Palette** — Use it instead when the same search-and-pick interaction is for navigating, not inserting steps.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport {\n  ArrowRightFromLine,\n  ArrowUpRight,\n  ChevronDown,\n  CircleArrowOutUpRight,\n  Code,\n  CornerDownRight,\n  EllipsisVertical,\n  Heading1,\n  Plus,\n  Shuffle,\n  Text as TextIcon,\n  Trash2,\n  Workflow,\n  X,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/* ── data model ── */\n\n/** An inline reference inside an instruction, e.g. \"Tag · Refund initiated\". */\nexport interface ProcedureChip {\n  /** Bold leading verb: \"Update\", \"Tag\", \"Use\", \"Read\". */\n  verb: string;\n  label: string;\n  icon?: React.ComponentType<{ className?: string }>;\n}\n\nexport type ProcedureSegment = string | ProcedureChip;\n\nexport type ProcedureActionKind = \"escalate\" | \"switch\" | \"workflow\" | \"end\";\n\nexport type ProcedureStep =\n  | { id: string; kind: \"instruction\"; content: ProcedureSegment[] }\n  | {\n      id: string;\n      kind: \"condition\";\n      mode: \"text\" | \"code\";\n      condition: string;\n      then: ProcedureStep[];\n      /** Present (possibly empty) when the condition has an ELSE branch. */\n      else?: ProcedureStep[];\n    }\n  | { id: string; kind: \"header\"; text: string }\n  /** `target` is the id of a top-level header step. */\n  | { id: string; kind: \"goto\"; target?: string }\n  | { id: string; kind: \"action\"; action: ProcedureActionKind; target?: string };\n\nexport interface ProcedureTargets {\n  escalate?: string[];\n  switch?: string[];\n  workflow?: string[];\n}\n\nexport interface ProcedureBuilderProps {\n  initialSteps?: ProcedureStep[];\n  /** Options offered by the Escalate / Switch / Run workflow pickers. */\n  targets?: ProcedureTargets;\n  /** Used in placeholders: \"Describe what {agentName} should do…\". */\n  agentName?: string;\n  onChange?: (steps: ProcedureStep[]) => void;\n  className?: string;\n}\n\n/** Transient row holding the \"/Search steps…\" input. Never passed to `onChange`. */\ntype DraftStep = { id: string; kind: \"draft\" };\ntype Step = ProcedureStep | DraftStep;\ntype ConditionStep = Extract<ProcedureStep, { kind: \"condition\" }>;\ntype Branch = \"then\" | \"else\";\n\n/* ── step catalogue ── */\n\ntype StepOption =\n  | \"instruction\"\n  | \"condition\"\n  | \"header\"\n  | \"goto\"\n  | ProcedureActionKind;\n\ninterface StepOptionMeta {\n  label: string;\n  /** Shorter/longer label used inside a condition branch. */\n  nestedLabel?: string;\n  icon: React.ComponentType<{ className?: string }>;\n  badge: string;\n  group: \"structure\" | \"action\";\n}\n\nconst BADGE = {\n  neutral: \"bg-muted text-foreground/80\",\n  condition: \"bg-violet-100 text-violet-700 dark:bg-violet-500/20 dark:text-violet-300\",\n  goto: \"bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300\",\n  action: \"bg-orange-100 text-orange-900 dark:bg-orange-500/20 dark:text-orange-200\",\n};\n\nconst STEP_OPTIONS: Record<StepOption, StepOptionMeta> = {\n  instruction: { label: \"Instruction\", icon: TextIcon, badge: BADGE.neutral, group: \"structure\" },\n  condition: { label: \"Condition\", icon: ArrowRightFromLine, badge: BADGE.condition, group: \"structure\" },\n  header: { label: \"Section header\", icon: Heading1, badge: BADGE.neutral, group: \"structure\" },\n  goto: { label: \"Go to\", nestedLabel: \"Go to section\", icon: ArrowUpRight, badge: BADGE.goto, group: \"structure\" },\n  escalate: { label: \"Escalate\", icon: CircleArrowOutUpRight, badge: BADGE.action, group: \"action\" },\n  switch: { label: \"Switch\", icon: Shuffle, badge: BADGE.action, group: \"action\" },\n  workflow: { label: \"Run workflow\", icon: Workflow, badge: BADGE.action, group: \"action\" },\n  end: { label: \"End\", nestedLabel: \"End procedure\", icon: X, badge: BADGE.action, group: \"action\" },\n};\n\n// Conditions and section headers only make sense at the top level, so a\n// branch offers a shorter menu.\nconst TOP_LEVEL_OPTIONS: StepOption[] = [\"instruction\", \"condition\", \"header\", \"goto\", \"escalate\", \"switch\", \"workflow\", \"end\"];\nconst NESTED_OPTIONS: StepOption[] = [\"instruction\", \"goto\", \"escalate\", \"switch\", \"workflow\", \"end\"];\n\nconst ACTION_COPY: Record<ProcedureActionKind, { prefix: string; picker: string }> = {\n  escalate: { prefix: \"Escalate to\", picker: \"Choose a team\" },\n  switch: { prefix: \"Switch to\", picker: \"Choose a procedure\" },\n  workflow: { prefix: \"Run\", picker: \"Choose a workflow\" },\n  end: { prefix: \"End procedure\", picker: \"\" },\n};\n\n/* ── tree helpers ── */\n\nlet idCounter = 0;\nfunction newId(prefix: string) {\n  idCounter += 1;\n  return `${prefix}-${Date.now().toString(36)}-${idCounter}`;\n}\n\nfunction mapSteps(steps: Step[], fn: (step: Step) => Step | null): Step[] {\n  const out: Step[] = [];\n  for (const step of steps) {\n    const next = fn(step);\n    if (!next) continue;\n    if (next.kind === \"condition\") {\n      out.push({\n        ...next,\n        then: mapSteps(next.then, fn) as ProcedureStep[],\n        else: next.else ? (mapSteps(next.else, fn) as ProcedureStep[]) : undefined,\n      });\n    } else {\n      out.push(next);\n    }\n  }\n  return out;\n}\n\nfunction insertAfter(steps: Step[], afterId: string, inserted: Step): Step[] {\n  const index = steps.findIndex((s) => s.id === afterId);\n  if (index !== -1) return [...steps.slice(0, index + 1), inserted, ...steps.slice(index + 1)];\n  return steps.map((s) =>\n    s.kind === \"condition\"\n      ? {\n          ...s,\n          then: insertAfter(s.then, afterId, inserted) as ProcedureStep[],\n          else: s.else ? (insertAfter(s.else, afterId, inserted) as ProcedureStep[]) : undefined,\n        }\n      : s\n  );\n}\n\nfunction stripDrafts(steps: Step[]): ProcedureStep[] {\n  return mapSteps(steps, (s) => (s.kind === \"draft\" ? null : s)) as ProcedureStep[];\n}\n\nfunction createStep(option: StepOption, id: string): ProcedureStep {\n  switch (option) {\n    case \"instruction\":\n      return { id, kind: \"instruction\", content: [\"\"] };\n    case \"condition\":\n      return {\n        id,\n        kind: \"condition\",\n        mode: \"text\",\n        condition: \"\",\n        then: [{ id: newId(\"step\"), kind: \"instruction\", content: [\"\"] }],\n      };\n    case \"header\":\n      return { id, kind: \"header\", text: \"\" };\n    case \"goto\":\n      return { id, kind: \"goto\" };\n    default:\n      return { id, kind: \"action\", action: option };\n  }\n}\n\nconst LETTERS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n\nconst PLACEHOLDER_SUFFIX: Record<Branch | \"top\", string> = {\n  top: \"\",\n  then: \" when this condition is met\",\n  else: \" when this condition isn't met\",\n};\n\n/* ── context ── */\n\ninterface BuilderContextValue {\n  agentName: string;\n  targets: ProcedureTargets;\n  sections: { id: string; text: string }[];\n  /** Id of the step that should grab focus when it mounts. */\n  focusId: string | null;\n  update: (id: string, fn: (step: Step) => Step | null) => void;\n  remove: (id: string) => void;\n  /** Opens a \"/Search steps…\" row right after `id`. */\n  draftAfter: (id: string) => void;\n  /** Opens a \"/Search steps…\" row at the end of a list (top level when `parentId` is null). */\n  draftAtEnd: (parentId: string | null, branch?: Branch) => void;\n  commitDraft: (draftId: string, option: StepOption) => void;\n}\n\nconst BuilderContext = React.createContext<BuilderContextValue | null>(null);\n\nfunction useBuilder() {\n  const ctx = React.useContext(BuilderContext);\n  if (!ctx) throw new Error(\"ProcedureBuilder parts must render inside <ProcedureBuilder>\");\n  return ctx;\n}\n\n/* ── root ── */\n\nexport function ProcedureBuilder({\n  initialSteps = [],\n  targets = {},\n  agentName = \"the agent\",\n  onChange,\n  className,\n}: ProcedureBuilderProps) {\n  const [steps, setSteps] = React.useState<Step[]>(initialSteps);\n  const [focusId, setFocusId] = React.useState<string | null>(null);\n\n  const onChangeRef = React.useRef(onChange);\n  React.useEffect(() => {\n    onChangeRef.current = onChange;\n  });\n  const isFirstRender = React.useRef(true);\n  React.useEffect(() => {\n    if (isFirstRender.current) {\n      isFirstRender.current = false;\n      return;\n    }\n    onChangeRef.current?.(stripDrafts(steps));\n  }, [steps]);\n\n  const sections = steps\n    .filter((s): s is Extract<ProcedureStep, { kind: \"header\" }> => s.kind === \"header\")\n    .map((s) => ({ id: s.id, text: s.text }));\n\n  // Only one draft row at a time: opening a new one replaces any other.\n  const withoutDrafts = (list: Step[]) => mapSteps(list, (s) => (s.kind === \"draft\" ? null : s));\n\n  const ctx: BuilderContextValue = {\n    agentName,\n    targets,\n    sections,\n    focusId,\n    update: (id, fn) => setSteps((prev) => mapSteps(prev, (s) => (s.id === id ? fn(s) : s))),\n    remove: (id) =>\n      setSteps((prev) =>\n        mapSteps(prev, (s) => {\n          if (s.id === id) return null;\n          // A goto pointing at a deleted section loses its target.\n          if (s.kind === \"goto\" && s.target === id) return { ...s, target: undefined };\n          return s;\n        })\n      ),\n    draftAfter: (id) => {\n      const draft: DraftStep = { id: newId(\"draft\"), kind: \"draft\" };\n      setFocusId(draft.id);\n      setSteps((prev) => insertAfter(withoutDrafts(prev), id, draft));\n    },\n    draftAtEnd: (parentId, branch = \"then\") => {\n      const draft: DraftStep = { id: newId(\"draft\"), kind: \"draft\" };\n      setFocusId(draft.id);\n      setSteps((prev) => {\n        const clean = withoutDrafts(prev);\n        if (parentId === null) return [...clean, draft];\n        return mapSteps(clean, (s) =>\n          s.id === parentId && s.kind === \"condition\"\n            ? { ...s, [branch]: [...(s[branch] ?? []), draft as unknown as ProcedureStep] }\n            : s\n        );\n      });\n    },\n    commitDraft: (draftId, option) => {\n      const id = newId(\"step\");\n      setFocusId(id);\n      setSteps((prev) => mapSteps(prev, (s) => (s.id === draftId ? createStep(option, id) : s)));\n    },\n  };\n\n  return (\n    <BuilderContext.Provider value={ctx}>\n      <div className={cn(\"w-full text-sm text-foreground\", className)}>\n        <StepList steps={steps} parentId={null} />\n        <button\n          type=\"button\"\n          onClick={() => ctx.draftAtEnd(null)}\n          className=\"ml-8 mt-3 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        >\n          <Plus className=\"size-3.5\" aria-hidden />\n          Add step\n        </button>\n      </div>\n    </BuilderContext.Provider>\n  );\n}\n\n/* ── lists ── */\n\nfunction StepList({ steps, parentId, branch }: { steps: Step[]; parentId: string | null; branch?: Branch }) {\n  const nested = parentId !== null;\n  // Section headers are unnumbered so the procedure's numbering stays a plain\n  // count of things the agent does.\n  const markers: string[] = [];\n  for (const step of steps) {\n    const count = markers.filter(Boolean).length;\n    markers.push(step.kind === \"header\" ? \"\" : nested ? `${LETTERS[count % 26]}.` : `${count + 1}.`);\n  }\n\n  return (\n    <ol className={cn(\"flex flex-col\", nested ? \"gap-2.5\" : \"gap-5\")}>\n      <AnimatePresence initial={false}>\n        {steps.map((step, index) => (\n          <motion.li\n            key={step.id}\n            layout=\"position\"\n            initial={{ opacity: 0, y: -4 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, transition: { duration: 0.12 } }}\n            transition={{ duration: 0.2, ease: \"easeOut\" }}\n            className=\"grid grid-cols-[2rem_minmax(0,1fr)] items-start\"\n          >\n            <span\n              aria-hidden\n              className={cn(\n                \"select-none font-mono text-[11px] tabular-nums text-muted-foreground\",\n                step.kind === \"condition\" ? \"pt-1\" : \"pt-[3px]\"\n              )}\n            >\n              {markers[index]}\n            </span>\n            <StepRow step={step} nested={nested} branch={branch} />\n          </motion.li>\n        ))}\n      </AnimatePresence>\n    </ol>\n  );\n}\n\nfunction StepRow({ step, nested, branch }: { step: Step; nested: boolean; branch?: Branch }) {\n  switch (step.kind) {\n    case \"draft\":\n      return <DraftRow id={step.id} nested={nested} />;\n    case \"instruction\":\n      return <InstructionRow step={step} branch={branch} />;\n    case \"condition\":\n      return <ConditionBlock step={step} />;\n    case \"header\":\n      return <HeaderRow step={step} />;\n    case \"goto\":\n      return <GotoRow step={step} />;\n    case \"action\":\n      return <ActionRow step={step} />;\n  }\n}\n\n/* ── draft row: the \"/\" step picker ── */\n\nfunction DraftRow({ id, nested }: { id: string; nested: boolean }) {\n  const { focusId, remove, commitDraft } = useBuilder();\n  const [query, setQuery] = React.useState(\"\");\n  const [active, setActive] = React.useState(0);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const listId = React.useId();\n\n  React.useEffect(() => {\n    if (focusId === id) inputRef.current?.focus();\n  }, [focusId, id]);\n\n  const options = (nested ? NESTED_OPTIONS : TOP_LEVEL_OPTIONS).filter((option) => {\n    const meta = STEP_OPTIONS[option];\n    const label = nested ? (meta.nestedLabel ?? meta.label) : meta.label;\n    return label.toLowerCase().includes(query.trim().toLowerCase());\n  });\n  const activeIndex = Math.min(active, Math.max(0, options.length - 1));\n\n  function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n    if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n      event.preventDefault();\n      if (!options.length) return;\n      const delta = event.key === \"ArrowDown\" ? 1 : -1;\n      setActive((activeIndex + delta + options.length) % options.length);\n    } else if (event.key === \"Enter\") {\n      event.preventDefault();\n      if (options[activeIndex]) commitDraft(id, options[activeIndex]);\n    } else if (event.key === \"Escape\" || (event.key === \"Backspace\" && query === \"\")) {\n      event.preventDefault();\n      remove(id);\n    }\n  }\n\n  const structure = options.filter((o) => STEP_OPTIONS[o].group === \"structure\");\n  const actions = options.filter((o) => STEP_OPTIONS[o].group === \"action\");\n\n  function renderOption(option: StepOption) {\n    const meta = STEP_OPTIONS[option];\n    const index = options.indexOf(option);\n    const Icon = meta.icon;\n    return (\n      <li\n        key={option}\n        id={`${listId}-${option}`}\n        role=\"option\"\n        aria-selected={index === activeIndex}\n        onPointerMove={() => setActive(index)}\n        // Keep focus in the input so blur doesn't dismiss the row mid-click.\n        onPointerDown={(event) => event.preventDefault()}\n        onClick={() => commitDraft(id, option)}\n        className={cn(\n          \"flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm\",\n          index === activeIndex ? \"bg-muted\" : \"text-foreground/90\"\n        )}\n      >\n        <span className={cn(\"flex size-6 shrink-0 items-center justify-center rounded-md\", meta.badge)}>\n          <Icon className=\"size-3.5\" aria-hidden />\n        </span>\n        {nested ? (meta.nestedLabel ?? meta.label) : meta.label}\n      </li>\n    );\n  }\n\n  return (\n    <div className=\"relative\">\n      <div className=\"inline-flex items-center rounded-md bg-muted/70 px-1.5 py-0.5\">\n        <span aria-hidden className=\"text-foreground/70\">\n          /\n        </span>\n        <input\n          ref={inputRef}\n          value={query}\n          onChange={(event) => {\n            setQuery(event.target.value);\n            setActive(0);\n          }}\n          onKeyDown={onKeyDown}\n          onBlur={() => remove(id)}\n          placeholder=\"Search steps…\"\n          role=\"combobox\"\n          aria-label=\"Add a step\"\n          aria-expanded\n          aria-controls={listId}\n          aria-autocomplete=\"list\"\n          aria-activedescendant={options[activeIndex] ? `${listId}-${options[activeIndex]}` : undefined}\n          className=\"w-28 bg-transparent text-sm placeholder:text-muted-foreground focus:outline-none\"\n        />\n      </div>\n\n      <motion.div\n        initial={{ opacity: 0, y: -4, scale: 0.98 }}\n        animate={{ opacity: 1, y: 0, scale: 1 }}\n        transition={{ duration: 0.14, ease: \"easeOut\" }}\n        style={{ transformOrigin: \"top left\" }}\n        className=\"absolute left-0 top-full z-30 mt-1.5 w-60 rounded-xl border bg-popover p-1.5 text-popover-foreground shadow-lg\"\n      >\n        <ul id={listId} role=\"listbox\" aria-label=\"Step types\">\n          {structure.map(renderOption)}\n          {structure.length > 0 && actions.length > 0 && <li role=\"separator\" className=\"-mx-1.5 my-1.5 h-px bg-border\" />}\n          {actions.map(renderOption)}\n          {options.length === 0 && <li className=\"px-2 py-1.5 text-sm text-muted-foreground\">No matching steps</li>}\n        </ul>\n      </motion.div>\n    </div>\n  );\n}\n\n/* ── instruction ── */\n\nfunction InstructionRow({ step, branch }: { step: Extract<ProcedureStep, { kind: \"instruction\" }>; branch?: Branch }) {\n  const { agentName, focusId, update, remove, draftAfter } = useBuilder();\n  const plain = step.content.every((segment) => typeof segment === \"string\");\n\n  // Instructions holding inline references render read-only here; a full\n  // editor would insert chips through an @-mention picker.\n  if (!plain) {\n    return (\n      <StepShell onRemove={() => remove(step.id)} label=\"instruction\">\n        <p className=\"leading-7 text-foreground/90\">\n          {step.content.map((segment, i) =>\n            typeof segment === \"string\" ? <React.Fragment key={i}>{segment}</React.Fragment> : <InlineChip key={i} chip={segment} />\n          )}\n        </p>\n      </StepShell>\n    );\n  }\n\n  const value = step.content.join(\"\");\n  return (\n    <StepShell onRemove={() => remove(step.id)} label=\"instruction\">\n      <AutoTextarea\n        value={value}\n        autoFocus={focusId === step.id}\n        aria-label=\"Instruction\"\n        placeholder={`Describe what ${agentName} should do${PLACEHOLDER_SUFFIX[branch ?? \"top\"]}`}\n        onChange={(text) => update(step.id, (s) => ({ ...(s as typeof step), content: [text] }))}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\" && !event.shiftKey) {\n            event.preventDefault();\n            draftAfter(step.id);\n          } else if (event.key === \"Backspace\" && value === \"\") {\n            event.preventDefault();\n            remove(step.id);\n          } else if (event.key === \"/\" && value === \"\") {\n            // Typing \"/\" in an empty instruction swaps it for the step picker.\n            event.preventDefault();\n            draftAfter(step.id);\n            remove(step.id);\n          }\n        }}\n        className=\"leading-7 text-foreground/90\"\n      />\n    </StepShell>\n  );\n}\n\nexport function InlineChip({ chip }: { chip: ProcedureChip }) {\n  const Icon = chip.icon;\n  return (\n    <span className=\"mx-0.5 inline-flex items-center gap-1 whitespace-nowrap rounded-full border border-border/60 bg-muted/60 px-2 py-px align-baseline text-[13px] leading-6\">\n      {Icon && <Icon className=\"size-3.5 text-foreground/80\" aria-hidden />}\n      <span className=\"font-semibold\">{chip.verb}</span>\n      <span className=\"text-foreground/80\">{chip.label}</span>\n      <EllipsisVertical className=\"-mr-1 size-3.5 text-muted-foreground\" aria-hidden />\n    </span>\n  );\n}\n\n/* ── section header ── */\n\nfunction HeaderRow({ step }: { step: Extract<ProcedureStep, { kind: \"header\" }> }) {\n  const { focusId, update, remove, draftAfter } = useBuilder();\n  return (\n    <StepShell onRemove={() => remove(step.id)} label=\"section\">\n      <AutoTextarea\n        value={step.text}\n        autoFocus={focusId === step.id}\n        aria-label=\"Section name\"\n        placeholder=\"Section name\"\n        onChange={(text) => update(step.id, (s) => ({ ...(s as typeof step), text }))}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\") {\n            event.preventDefault();\n            draftAfter(step.id);\n          } else if (event.key === \"Backspace\" && step.text === \"\") {\n            event.preventDefault();\n            remove(step.id);\n          }\n        }}\n        className=\"text-base font-semibold leading-7\"\n      />\n    </StepShell>\n  );\n}\n\n/* ── go to / actions ── */\n\nfunction GotoRow({ step }: { step: Extract<ProcedureStep, { kind: \"goto\" }> }) {\n  const { sections, focusId, update, remove } = useBuilder();\n  const options = sections.map((s) => ({ value: s.id, label: s.text || \"Untitled section\" }));\n  return (\n    <StepShell onRemove={() => remove(step.id)} label=\"go to step\">\n      <div className=\"flex flex-wrap items-center gap-2 leading-7\">\n        <StepBadge icon={ArrowUpRight} className={BADGE.goto} />\n        <span>Go to</span>\n        <TargetPicker\n          value={step.target}\n          options={options}\n          placeholder=\"Choose a section\"\n          emptyLabel=\"Add a section header first\"\n          autoOpen={focusId === step.id}\n          onSelect={(target) => update(step.id, (s) => ({ ...(s as typeof step), target }))}\n        />\n      </div>\n    </StepShell>\n  );\n}\n\nfunction ActionRow({ step }: { step: Extract<ProcedureStep, { kind: \"action\" }> }) {\n  const { targets, focusId, update, remove } = useBuilder();\n  const meta = STEP_OPTIONS[step.action];\n  const copy = ACTION_COPY[step.action];\n  const options = step.action === \"end\" ? [] : (targets[step.action] ?? []).map((t) => ({ value: t, label: t }));\n\n  return (\n    <StepShell onRemove={() => remove(step.id)} label={`${meta.label} step`}>\n      <div className=\"flex flex-wrap items-center gap-2 leading-7\">\n        <StepBadge icon={meta.icon} className={BADGE.action} />\n        <span>{copy.prefix}</span>\n        {step.action !== \"end\" && (\n          <TargetPicker\n            value={step.target}\n            options={options}\n            placeholder={copy.picker}\n            emptyLabel=\"Nothing to choose yet\"\n            autoOpen={focusId === step.id}\n            onSelect={(target) => update(step.id, (s) => ({ ...(s as typeof step), target }))}\n          />\n        )}\n      </div>\n    </StepShell>\n  );\n}\n\nfunction TargetPicker({\n  value,\n  options,\n  placeholder,\n  emptyLabel,\n  autoOpen,\n  onSelect,\n}: {\n  value?: string;\n  options: { value: string; label: string }[];\n  placeholder: string;\n  emptyLabel: string;\n  autoOpen: boolean;\n  onSelect: (value: string) => void;\n}) {\n  const [open, setOpen] = React.useState(autoOpen);\n  const [active, setActive] = React.useState(0);\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const buttonRef = React.useRef<HTMLButtonElement>(null);\n  const listRef = React.useRef<HTMLUListElement>(null);\n  const listId = React.useId();\n  const selected = options.find((o) => o.value === value);\n\n  useDismiss(rootRef, open, () => setOpen(false));\n\n  React.useEffect(() => {\n    if (open) listRef.current?.focus();\n  }, [open]);\n\n  function close(refocus: boolean) {\n    setOpen(false);\n    if (refocus) buttonRef.current?.focus();\n  }\n\n  function onListKeyDown(event: React.KeyboardEvent) {\n    if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n      event.preventDefault();\n      if (!options.length) return;\n      const delta = event.key === \"ArrowDown\" ? 1 : -1;\n      setActive((i) => (i + delta + options.length) % options.length);\n    } else if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      if (options[active]) {\n        onSelect(options[active].value);\n        close(true);\n      }\n    } else if (event.key === \"Escape\") {\n      event.preventDefault();\n      close(true);\n    } else if (event.key === \"Tab\") {\n      setOpen(false);\n    }\n  }\n\n  return (\n    <div ref={rootRef} className=\"relative inline-block\">\n      <button\n        ref={buttonRef}\n        type=\"button\"\n        aria-haspopup=\"listbox\"\n        aria-expanded={open}\n        aria-controls={open ? listId : undefined}\n        onClick={() => {\n          setActive(Math.max(0, options.findIndex((o) => o.value === value)));\n          setOpen((o) => !o);\n        }}\n        className={cn(\n          \"inline-flex items-center gap-1 rounded-full border border-border/60 bg-muted/60 px-2.5 text-[13px] leading-6 transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          !selected && \"text-muted-foreground\"\n        )}\n      >\n        {selected?.label ?? placeholder}\n        <ChevronDown className=\"size-3.5 opacity-60\" aria-hidden />\n      </button>\n      <AnimatePresence>\n        {open && (\n          <motion.ul\n            ref={listRef}\n            id={listId}\n            role=\"listbox\"\n            tabIndex={-1}\n            aria-activedescendant={options[active] ? `${listId}-${active}` : undefined}\n            onKeyDown={onListKeyDown}\n            initial={{ opacity: 0, y: -4, scale: 0.98 }}\n            animate={{ opacity: 1, y: 0, scale: 1 }}\n            exit={{ opacity: 0, y: -4, scale: 0.98, transition: { duration: 0.1 } }}\n            transition={{ duration: 0.14, ease: \"easeOut\" }}\n            style={{ transformOrigin: \"top left\" }}\n            className=\"absolute left-0 top-full z-30 mt-1.5 min-w-52 rounded-xl border bg-popover p-1.5 text-popover-foreground shadow-lg focus:outline-none\"\n          >\n            {options.map((option, index) => (\n              <li\n                key={option.value}\n                id={`${listId}-${index}`}\n                role=\"option\"\n                aria-selected={option.value === value}\n                onPointerMove={() => setActive(index)}\n                onClick={() => {\n                  onSelect(option.value);\n                  close(true);\n                }}\n                className={cn(\n                  \"cursor-pointer whitespace-nowrap rounded-lg px-2 py-1.5 text-sm\",\n                  index === active && \"bg-muted\",\n                  option.value === value && \"font-medium\"\n                )}\n              >\n                {option.label}\n              </li>\n            ))}\n            {options.length === 0 && <li className=\"px-2 py-1.5 text-sm text-muted-foreground\">{emptyLabel}</li>}\n          </motion.ul>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\n/* ── condition ── */\n\nfunction ConditionBlock({ step }: { step: ConditionStep }) {\n  const { focusId, update, remove, draftAtEnd } = useBuilder();\n  const set = (patch: Partial<ConditionStep>) => update(step.id, (s) => ({ ...(s as ConditionStep), ...patch }));\n  const hasElse = step.else !== undefined;\n\n  return (\n    <div className=\"flex flex-col\">\n      <BranchHeader\n        icon={ArrowRightFromLine}\n        label=\"IF\"\n        menuLabel=\"Condition options\"\n        items={[\n          hasElse\n            ? { label: \"Remove ELSE branch\", onSelect: () => set({ else: undefined }) }\n            : { label: \"Add ELSE branch\", onSelect: () => set({ else: [{ id: newId(\"step\"), kind: \"instruction\", content: [\"\"] }] }) },\n          step.mode === \"text\"\n            ? { label: \"Write as code\", onSelect: () => set({ mode: \"code\" }) }\n            : { label: \"Write in plain language\", onSelect: () => set({ mode: \"text\" }) },\n          { label: \"Delete condition\", destructive: true, onSelect: () => remove(step.id) },\n        ]}\n      />\n\n      <BranchBody onAdd={() => draftAtEnd(step.id, \"then\")} addLabel=\"Add a step to this condition\">\n        <ConditionField\n          value={step.condition}\n          mode={step.mode}\n          autoFocus={focusId === step.id}\n          onChange={(condition) => set({ condition })}\n          onToggleMode={() => set({ mode: step.mode === \"text\" ? \"code\" : \"text\" })}\n        />\n        <div className=\"mt-3\">\n          <StepList steps={step.then} parentId={step.id} branch=\"then\" />\n        </div>\n      </BranchBody>\n\n      <AnimatePresence initial={false}>\n        {hasElse && (\n          <motion.div\n            key=\"else\"\n            initial={{ opacity: 0, y: -6 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -6, transition: { duration: 0.12 } }}\n            transition={{ duration: 0.2, ease: \"easeOut\" }}\n          >\n            <BranchHeader\n              icon={CornerDownRight}\n              label=\"ELSE\"\n              menuLabel=\"Else options\"\n              items={[{ label: \"Remove ELSE branch\", destructive: true, onSelect: () => set({ else: undefined }) }]}\n            />\n            <BranchBody onAdd={() => draftAtEnd(step.id, \"else\")} addLabel=\"Add a step to the else branch\">\n              <div className=\"pt-1\">\n                <StepList steps={step.else ?? []} parentId={step.id} branch=\"else\" />\n              </div>\n            </BranchBody>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction BranchHeader({\n  icon,\n  label,\n  menuLabel,\n  items,\n}: {\n  icon: React.ComponentType<{ className?: string }>;\n  label: string;\n  menuLabel: string;\n  items: MenuItem[];\n}) {\n  return (\n    <div className=\"flex items-center gap-2\">\n      <StepBadge icon={icon} className={BADGE.condition} />\n      <span className=\"font-mono text-[13px] font-medium tracking-wide\">{label}</span>\n      <KebabMenu label={menuLabel} items={items} />\n    </div>\n  );\n}\n\nfunction BranchBody({ children, onAdd, addLabel }: { children: React.ReactNode; onAdd: () => void; addLabel: string }) {\n  return (\n    // The rail runs down from the centre of the 1.5rem badge.\n    <div className=\"relative ml-3 border-l border-border pb-10 pl-5 pt-2\">\n      {children}\n      <button\n        type=\"button\"\n        onClick={onAdd}\n        aria-label={addLabel}\n        className=\"absolute bottom-0 left-0 flex size-6 -translate-x-1/2 items-center justify-center rounded-full border bg-background text-foreground/80 shadow-xs transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <Plus className=\"size-3.5\" aria-hidden />\n      </button>\n    </div>\n  );\n}\n\nfunction ConditionField({\n  value,\n  mode,\n  autoFocus,\n  onChange,\n  onToggleMode,\n}: {\n  value: string;\n  mode: \"text\" | \"code\";\n  autoFocus: boolean;\n  onChange: (value: string) => void;\n  onToggleMode: () => void;\n}) {\n  const code = mode === \"code\";\n  return (\n    <div className=\"group/condition relative rounded-xl border bg-card px-3.5 py-2.5 pr-11 shadow-xs transition-shadow focus-within:border-ring/50 focus-within:ring-2 focus-within:ring-ring/15\">\n      <div className=\"relative\">\n        {code && (\n          <pre\n            aria-hidden\n            className=\"pointer-events-none absolute inset-0 whitespace-pre-wrap break-words font-mono text-[13px] leading-6\"\n          >\n            <HighlightedCode source={value} />\n            {/* Keeps a trailing newline from collapsing the overlay height. */}{\" \"}\n          </pre>\n        )}\n        <AutoTextarea\n          value={value}\n          autoFocus={autoFocus}\n          aria-label={code ? \"Condition expression\" : \"Condition\"}\n          placeholder={code ? \"order.total > 100\" : \"Describe when this applies, e.g. the customer reports a damaged item\"}\n          spellCheck={!code}\n          onChange={onChange}\n          className={cn(\n            code\n              ? \"font-mono text-[13px] leading-6 text-transparent caret-foreground selection:bg-blue-500/25\"\n              : \"leading-7 text-foreground/90\"\n          )}\n        />\n      </div>\n      <button\n        type=\"button\"\n        onClick={onToggleMode}\n        aria-pressed={code}\n        aria-label={code ? \"Write in plain language\" : \"Write as code\"}\n        title={code ? \"Write in plain language\" : \"Write as code\"}\n        className={cn(\n          \"absolute right-2 top-2 flex size-7 items-center justify-center rounded-full border bg-background text-foreground/70 transition-[opacity,background-color] hover:bg-muted focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          code ? \"opacity-100\" : \"opacity-0 group-hover/condition:opacity-100 group-focus-within/condition:opacity-100\"\n        )}\n      >\n        <Code className=\"size-3.5\" aria-hidden />\n      </button>\n    </div>\n  );\n}\n\nconst CODE_KEYWORDS = new Set([\"and\", \"or\", \"not\", \"in\", \"true\", \"false\", \"null\"]);\n\nfunction HighlightedCode({ source }: { source: string }) {\n  const tokens = source.match(/\\s+|\"[^\"]*\"?|'[^']*'?|\\d+(?:\\.\\d+)?|[A-Za-z_][\\w.]*|./g) ?? [];\n  return tokens.map((token, i) => {\n    let className = \"text-muted-foreground\";\n    if (/^\\s+$/.test(token)) className = \"\";\n    else if (/^[\"']/.test(token)) className = \"text-emerald-700 dark:text-emerald-400\";\n    else if (/^\\d/.test(token)) className = \"text-amber-700 dark:text-amber-400\";\n    else if (CODE_KEYWORDS.has(token)) className = \"text-foreground\";\n    else if (/^[A-Za-z_]/.test(token)) className = \"text-blue-700 dark:text-blue-400\";\n    return (\n      <span key={i} className={className}>\n        {token}\n      </span>\n    );\n  });\n}\n\n/* ── shared bits ── */\n\nfunction StepBadge({ icon: Icon, className }: { icon: React.ComponentType<{ className?: string }>; className: string }) {\n  return (\n    <span className={cn(\"flex size-6 shrink-0 items-center justify-center rounded-md\", className)}>\n      <Icon className=\"size-3.5\" aria-hidden />\n    </span>\n  );\n}\n\n/** Row wrapper with a hover-revealed remove button. */\nfunction StepShell({ children, onRemove, label }: { children: React.ReactNode; onRemove: () => void; label: string }) {\n  return (\n    <div className=\"group/step relative pr-8\">\n      {children}\n      <button\n        type=\"button\"\n        onClick={onRemove}\n        aria-label={`Remove ${label}`}\n        className=\"absolute right-0 top-0.5 flex size-6 items-center justify-center rounded-md text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/step:opacity-100\"\n      >\n        <Trash2 className=\"size-3.5\" aria-hidden />\n      </button>\n    </div>\n  );\n}\n\ninterface MenuItem {\n  label: string;\n  destructive?: boolean;\n  onSelect: () => void;\n}\n\nfunction KebabMenu({ label, items }: { label: string; items: MenuItem[] }) {\n  const [open, setOpen] = React.useState(false);\n  const [active, setActive] = React.useState(0);\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const buttonRef = React.useRef<HTMLButtonElement>(null);\n  const itemRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n  const menuId = React.useId();\n\n  useDismiss(rootRef, open, () => setOpen(false));\n\n  React.useEffect(() => {\n    if (open) itemRefs.current[active]?.focus();\n  }, [open, active]);\n\n  function close() {\n    setOpen(false);\n    buttonRef.current?.focus();\n  }\n\n  function onKeyDown(event: React.KeyboardEvent) {\n    const last = items.length - 1;\n    if (event.key === \"ArrowDown\") setActive((i) => (i === last ? 0 : i + 1));\n    else if (event.key === \"ArrowUp\") setActive((i) => (i === 0 ? last : i - 1));\n    else if (event.key === \"Home\") setActive(0);\n    else if (event.key === \"End\") setActive(last);\n    else if (event.key === \"Escape\") close();\n    else if (event.key === \"Tab\") setOpen(false);\n    else return;\n    if (event.key !== \"Tab\") event.preventDefault();\n  }\n\n  return (\n    <div ref={rootRef} className=\"relative\">\n      <button\n        ref={buttonRef}\n        type=\"button\"\n        aria-label={label}\n        aria-haspopup=\"menu\"\n        aria-expanded={open}\n        aria-controls={open ? menuId : undefined}\n        onClick={() => {\n          setActive(0);\n          setOpen((o) => !o);\n        }}\n        className=\"flex size-6 items-center justify-center rounded-md text-foreground/70 transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <EllipsisVertical className=\"size-3.5\" aria-hidden />\n      </button>\n      <AnimatePresence>\n        {open && (\n          <motion.div\n            id={menuId}\n            role=\"menu\"\n            aria-label={label}\n            onKeyDown={onKeyDown}\n            initial={{ opacity: 0, y: -4, scale: 0.98 }}\n            animate={{ opacity: 1, y: 0, scale: 1 }}\n            exit={{ opacity: 0, y: -4, scale: 0.98, transition: { duration: 0.1 } }}\n            transition={{ duration: 0.14, ease: \"easeOut\" }}\n            style={{ transformOrigin: \"top left\" }}\n            className=\"absolute left-0 top-full z-30 mt-1 min-w-48 rounded-xl border bg-popover p-1.5 text-popover-foreground shadow-lg\"\n          >\n            {items.map((item, index) => (\n              <button\n                key={item.label}\n                ref={(el) => {\n                  itemRefs.current[index] = el;\n                }}\n                type=\"button\"\n                role=\"menuitem\"\n                tabIndex={index === active ? 0 : -1}\n                onPointerMove={() => setActive(index)}\n                onClick={() => {\n                  close();\n                  item.onSelect();\n                }}\n                className={cn(\n                  \"flex w-full items-center rounded-lg px-2 py-1.5 text-left text-sm focus:bg-muted focus:outline-none\",\n                  item.destructive && \"text-red-600 dark:text-red-400\"\n                )}\n              >\n                {item.label}\n              </button>\n            ))}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction AutoTextarea({\n  value,\n  onChange,\n  autoFocus,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"textarea\">, \"onChange\" | \"value\"> & {\n  value: string;\n  onChange: (value: string) => void;\n}) {\n  const ref = React.useRef<HTMLTextAreaElement>(null);\n\n  React.useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    el.style.height = \"0px\";\n    el.style.height = `${el.scrollHeight}px`;\n  }, [value]);\n\n  React.useEffect(() => {\n    if (autoFocus) ref.current?.focus();\n  }, [autoFocus]);\n\n  return (\n    <textarea\n      ref={ref}\n      rows={1}\n      value={value}\n      onChange={(event) => onChange(event.target.value)}\n      className={cn(\n        \"block w-full resize-none overflow-hidden bg-transparent p-0 placeholder:text-muted-foreground focus:outline-none\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction useDismiss(ref: React.RefObject<HTMLElement | null>, open: boolean, onDismiss: () => void) {\n  const onDismissRef = React.useRef(onDismiss);\n  React.useEffect(() => {\n    onDismissRef.current = onDismiss;\n  });\n  React.useEffect(() => {\n    if (!open) return;\n    function onPointerDown(event: PointerEvent) {\n      if (ref.current && !ref.current.contains(event.target as Node)) onDismissRef.current();\n    }\n    document.addEventListener(\"pointerdown\", onPointerDown);\n    return () => document.removeEventListener(\"pointerdown\", onPointerDown);\n  }, [open, ref]);\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}