# Imbrace CLI

> Imbrace CLI allows coding agents and developers to interact with the Imbrace platform via command line. Imbrace is a CRM platform supporting AI chat assistants, data boards (CRM pipelines), workflows, and more.

Things to remember when using Imbrace CLI:

- Always run `imbrace login` first. Credentials are stored via the `conf` package — exact path depends on OS:
  - macOS: `~/Library/Preferences/imbrace-nodejs/config.json`
  - Linux: `~/.config/imbrace-nodejs/config.json`
  - Windows: `%APPDATA%\imbrace-nodejs\Config\config.json`
- If a command returns a 401 error, run `imbrace login` again to refresh credentials.
- All commands support `--json` flag for machine-readable output — always use `--json` when scripting or using a coding agent.
- Run `imbrace data-board list --json` first to get Board IDs (`brd_...`) before using other data-board commands.
- Board items use the format `{ fields: [{ board_field_id, value }] }` — field IDs are fetched automatically in interactive mode.
- Field types available (16): `ShortText`, `LongText`, `Number`, `Date`, `Email`, `Phone`, `Currency`, `SingleSelection`, `MultipleSelection`, `Checkbox`, `Assignee`, `MultipleAssignee`, `Link`, `Notes`, `Origin`, `Priority`. **Do NOT use `Dropdown`** — backend rejects it (use `SingleSelection` instead).
- Run `imbrace ai-agent list --json` first to get Agent IDs before using other ai-agent commands.

## Profiles (AWS-style multi-account)

Credentials live in **named profiles** so one user can manage multiple accounts / environments (work + personal, prod + sandbox, etc.). The CLI ships with a single "default" profile out of the box; legacy single-credential configs are auto-migrated.

Resolution order (highest priority first):
1. `--profile <name>` flag (per-call override)
2. `IMBRACE_PROFILE=<name>` env var
3. `active_profile` saved in the config file
4. `"default"` fallback

Commands:
- `imbrace profile list --json` — list all profiles, mark the active one
- `imbrace profile show [name] --json` — print details (env, email, org_id, timeout, ...)
- `imbrace profile create <name> --api-key api_xxx... [--env stable|sandbox|develop|prodv2] [--org-id <org_id>] [--base-url <url>] [--timeout <ms>] [--check-health] [--services '<json>'] [--force]` — verifies the credential before saving (`--force` skips)
- `imbrace profile use <name>` — switch active profile
- `imbrace profile delete <name> --yes`
- `imbrace profile rename <from> <to>`

Each profile maps 1:1 to SDK `ImbraceClientConfig`:

| Profile field | SDK option | Notes |
|---|---|---|
| `credential` (api_*/sk-*) | `apiKey` | server-side key |
| `credential` (JWT) | `accessToken` | OAuth/login token |
| `env` | `env` | `stable` (default) / `sandbox` / `develop` / `prodv2` |
| `base_url` | `baseUrl` | overrides env preset |
| `organization_id` | `organizationId` | sent as `x-organization-id` header |
| `timeout` | `timeout` | request timeout in ms (default 30000) |
| `services` | `services` | `Partial<ServiceUrls>` — per-microservice URL override (advanced) |
| `check_health` | `checkHealth` | ping `/global/health` on init (default false) |

`imbrace login` / `logout` accept `--profile <name>` to target a specific profile (default: active).

Example — work + sandbox setup:
```bash
imbrace profile create work    --api-key api_aaa... --env stable
imbrace profile create sandbox --api-key api_bbb... --env sandbox
imbrace profile use work             # active
imbrace workflow list --profile sandbox   # per-call override
IMBRACE_PROFILE=sandbox imbrace workflow list  # env override
```

## Authentication

- `imbrace login --api-key api_xxx...` — Login with API key (recommended for CI/CD and coding agents)
- `imbrace login --email user@example.com --password mypass` — Login with email and password
- `imbrace logout` — Clear saved credentials
- `imbrace whoami --json` — Verify current authentication status

## Built-in commands

- `imbrace help [command]` — Show help for any command
- `imbrace autocomplete [shell]` — Display shell completion install instructions (bash, zsh, fish, powershell)
- `imbrace --version` — Print CLI version
- `imbrace docs` — Print the bundled `llms.txt` (this file). Pipe to a file (`imbrace docs > llms.txt`) and feed into a coding agent's context for one-shot reference. `--path` prints just the absolute path; `--json` outputs `{ path, content }`.
- `imbrace update` — Self-update the CLI to the latest npm version. `--check` checks without installing. `--version <x>` pins to a specific version. Available from v0.6.8 onwards.

## Data Board Commands

- `imbrace data-board list --json` — List all boards and their IDs (`brd_...`)
- `imbrace data-board create --name "Sales Pipeline" --json` — Create a new board
- `imbrace data-board create-field <boardId> --name "Company" --type ShortText --json` — Add a field to a board
- `imbrace data-board create-item <boardId> --fields '[{"board_field_id":"<id>","value":"Acme"}]' --json` — Create a record (Name field is required)
- `imbrace data-board list-items --board-id <id> --limit 20 --skip 0 --json` — List records with pagination
- `imbrace data-board list-items --board-id <id> --q "Acme" --json` — Full-text search records
- `imbrace data-board update-item <boardId> <itemId> --data '[{"key":"<fieldId>","value":"New"}]' --json` — Update a record
- `imbrace data-board delete-item <boardId> <itemId> --yes --json` — Delete a record
- `imbrace data-board export-csv --board-id <id> --out ./board.csv` — Export board to CSV file

## AI Agent Commands

**CRUD:**
- `imbrace ai-agent list --json` — List all AI agents and their IDs
- `imbrace ai-agent get <id> --json` — Get details of an AI agent
- `imbrace ai-agent create --name "Sales Bot" --json` — Create (minimal — defaults to system provider, "Default" model)
- `imbrace ai-agent create --name "X" --instructions "..." --personality "..." --tone "..." --json` — Create with full Behavior Settings
- `imbrace ai-agent update <id> --name "New Name" --json` — Update any subset of fields (others preserved via PUT-merge)
- `imbrace ai-agent update <id> --temperature 0.7 --tone "Casual" --json` — Update Behavior Settings
- `imbrace ai-agent delete <id> --yes --json` — Delete an AI agent

**Discovery (LLM providers/models):**
- `imbrace ai-agent list-providers --json` — List LLM providers (system + custom). Returns the UUID `provider_id` to pass to `--provider-id`.
- `imbrace ai-agent list-models --provider-id <id> --json` — List models for a provider. Use `system` for the default provider.

**Discovery (Knowledge Hub):**
- `imbrace ai-agent list-folders [--search <q>] --json` — List Knowledge Hub folders for `--folder-ids`.
- `imbrace ai-agent list-files --folder-id <id> --json` — List files inside a folder for `--file-ids`.

### AI Agent flags

`create` and `update` accept the same flags. `update` lets you change any subset; unspecified flags stay unchanged.

**Identity:**
- `--name` / `-n` — Agent name (required for create)
- `--description` / `-d` — Short description (shown under the title in UI list)
- `--instructions` / `-i` — System prompt sent on every chat

**Model:**
- `--model` — LLM model name (default: `Default` for system provider). Discover via `list-models`. **System provider only has one model named `Default`** — passing other names (like `gpt-4o`) makes the UI dropdown render empty even though the agent saves.
- `--provider-id` — LLM provider ID (default: `system`). Discover via `list-providers`. **Pass the UUID `provider_id`, NOT the MongoDB `_id`** — the UI matches dropdowns against the UUID.
- `--mode` — `standard` | `advanced` (default: standard)
- `--temperature` — 0.0–2.0 (default: 0.1). Lower = deterministic, higher = creative.

**Behavior Settings (UI tab):**
- `--personality` — Persona / role description → `personality_role`
- `--core-task` — Main tasks the agent performs → `core_task`
- `--tone` — Tone and style → `tone_and_style`
- `--response-length` — `short` | `medium` | `long`
- `--banned-words` — Comma-separated. **Word-level filter** on outputs (for topic refusal use `--instructions`).
- `--category` — `Support` | `Sales` | `Marketing` | `Team` | `Other`. Default: `Support`.
- `--guardrail-id` — Attach a guardrail by ID
- `--preload-information` — Static info auto-injected into context every chat

**Knowledge Support (UI tab) — comma-separated IDs:**
- `--folder-ids "id1,id2"` — Knowledge Hub folder IDs (RAG sources). Discover via `list-folders`.
- `--default-folder-id <id>` — Default folder for new files
- `--knowledge-hubs "id1,id2"` — Knowledge Hub IDs
- `--board-ids "brd_xxx,brd_yyy"` — Document Model (data board) IDs. Discover via `imbrace data-board list`.
- `--file-ids "id1,id2"` — Pre-uploaded file IDs. Discover via `list-files --folder-id <id>`.

**Runtime toggles (boolean, support `--no-X`):**
- `--show-thinking` / `--no-show-thinking` — Show model's thinking process (default: false)
- `--streaming` / `--no-streaming` — Stream response token-by-token (default: true)
- `--use-memory` / `--no-use-memory` — Remember conversation context across turns (default: true). **Known issue:** `--no-use-memory` at create may not stick (backend default override) — workaround: update after create.

**Other:**
- `--yes` / `-y` — Skip confirmation on delete
- `--json` — Full JSON output (machine-readable)
- `--id-only` — Print only the new agent ID (pipe-friendly, no jq). On `create` only.
- `-h` / `--help` — Show usage

### Full create example (Behavior Settings + custom provider + Knowledge)

```bash
# 1. Discover provider + model
imbrace ai-agent list-providers --json
imbrace ai-agent list-models --provider-id e2629292-7e9f-4d55-ba18-6827747eab33 --json

# 2. Discover Knowledge Hub folder + board (Document Model)
imbrace ai-agent list-folders --search support --json
imbrace data-board list --json

# 3. Create with full settings
imbrace ai-agent create \
  --name "Customer Support Specialist" \
  --description "Senior AI customer support agent" \
  --instructions "You are a senior customer support specialist..." \
  --personality "Friendly and professional" \
  --core-task "Answer product inquiries, help track orders" \
  --tone "Polite, professional, warm" \
  --response-length "medium" \
  --banned-words "stupid, idiot" \
  --category "Support" \
  --provider-id "e2629292-7e9f-4d55-ba18-6827747eab33" \
  --model "gpt-4o-mini" \
  --temperature 0.3 \
  --folder-ids "69bb82faa2cc764639bc6bdb" \
  --board-ids "brd_e5450d76-84d4-4c34-8b13-3d0f1873b53b" \
  --json
```

### How AI Agent create works (under the hood)

`imbrace ai-agent create` calls `client.agent.createUseCase({ usecase, assistant })` from `@imbrace/sdk`, which atomically creates:
1. An AI assistant (model, instructions, prompt)
2. A web channel (for chat widget demo)
3. A use-case template (the card shown on `cloud.imbrace.co/ai-agent`)

The `workflow_name` field is auto-generated as `<slug>_v<timestamp>` to ensure uniqueness — required by backend, must be snake_case (no spaces or special characters).

### Full request body sent to backend

The route mirrors the frontend `createCustomUseCase` payload (see `new-frontend/src/pages/AIAssistantManagement/useAIAssistantFormHook.tsx`):

```ts
{
  usecase: { title, short_description, agent_type, demo_url, supported_channels },
  assistant: {
    // Identity
    name, description, instructions, workflow_name, credential_name,
    // Model
    provider_id: "system", model_id: "gpt-4o",
    // Agent scope
    agent_type: "agent", mode: "standard", version: 2, channel, category,
    // Behavior (Behavior Settings tab in UI)
    personality_role, core_task, tone_and_style, response_length,
    banned_words, preload_information, guardrail_id, show_thinking_process,
    // Runtime
    streaming: true, use_memory: true, temperature: 0.1,
    // Knowledge (Knowledge Support tab in UI)
    knowledge_hubs: [], folder_ids: [], default_folder_id, board_ids: [], file_ids: [],
    // Advanced (Advanced Settings tab in UI)
    workflow_function_call: [], sub_agents: [], team_leads: [],
    // RAG / tooling
    metadata: {
      other_requirements: [], channel_id: "", team_ids: [], tool_server: null,
      enable_echart: false, top_k_relevant_results: 3, top_k: 40, max_steps: 10
    }
  }
}
```

**Fields not yet exposed via CLI flags** (set to defaults — modify via UI or future CLI updates): `channel`, `preload_information`, `knowledge_hubs`, `folder_ids`, `default_folder_id`, `board_ids`, `file_ids`, `workflow_function_call`, `sub_agents`, `team_leads`, `metadata.tool_server`, `metadata.team_ids`, `metadata.other_requirements`.

**IMPORTANT — All AI Agent content must be in English.** This includes `--name`, `--description`, and `--instructions`. Reasons:
1. The slug for `workflow_name` is derived from the name. Vietnamese diacritics get stripped and produce unreadable slugs (e.g. `"Trợ lý hỗ trợ"` → `tr_l_h_tr_v...`).
2. English content keeps logs, search, and downstream LLM behavior consistent.

If the user describes an agent in Vietnamese, translate the full payload to English before calling `imbrace ai-agent create`.

### Agent types

`imbrace ai-agent create` defaults to `agent_type: "agent"`. Override with `--agent-type` (options: `agent | assistant | conversational | workflow`). For `document_ai`, use the dedicated topic below.

## Orchestrator Commands

An Orchestrator is an AI Agent stored with `agent_type: "team_lead"` that delegates work to `sub_agents` / `team_leads`. Mirrors the "Orchestrator" choice in the UI's Create dialog.

- `imbrace orchestrator list --json` — Filters AI Agents where `agent_type=team_lead` (client-side; SDK has no server filter).
- `imbrace orchestrator get <id> --json` — Fetches the use case + underlying assistant; sub_agents are stored on the assistant.
- `imbrace orchestrator create -n "Name" -i "Routing instructions" --sub-agents id1,id2 [--team-leads id3] [--model] [--provider-id] [--temperature] --json`
- `imbrace orchestrator delete <id> --yes --json`

**Implementation notes** (the platform has 2 quirks the CLI works around for you — useful to know if you debug):
- Orchestrator-ness is encoded as `agent_type: "team_lead"` (NOT a separate `is_orchestrator` boolean). The webapp does the same conversion.
- `sub_agents` / `team_leads` must be **assistant IDs (UUIDs)**, not use-case IDs (`uc_*`). CLI auto-resolves any `uc_*` you pass via `client.agent.get()` lookup.
- `createUseCase` ignores both fields above on the assistant payload, so the CLI does a 2-step: create → `chatAi.updateAiAgent` PUT to apply them.

## Guardrail Commands

A Guardrail is a content-safety / compliance layer attached to AI Agents via `--guardrail-id`. Mirrors the "Guard Rail" choice in the UI's Create dialog.

- `imbrace guardrail list --json`
- `imbrace guardrail get <id> --json`
- `imbrace guardrail create -n "Name" -i "Block PII" [--model nim-nemo|model-armor] [--guardrail-provider-id <uuid>] [--unsafe-categories "violence,hate"] [--custom-unsafe-patterns "regex1,regex2"] [--competitor-keywords "X,Y"] --json`
- `imbrace guardrail update <id> -n -i --model [partial] --json` — PUT (full replace; name/model/instructions required).
- `imbrace guardrail delete <id> --yes --json`

**Implementation notes** (CLI handles these for you — useful when reading raw SDK output):
- Default model is `nim-nemo` (NVIDIA NIM Nemo). `model-armor` (Google) is the other built-in option; for any other model, pass `--guardrail-provider-id` referencing a custom guardrail provider.
- Backend requires `org_id` — CLI auto-fetches it via `client.account.getAccount()`. Override with `--org-id` if needed.
- `model-armor` ignores `instructions`, `custom_unsafe_patterns`, and `competitor_keywords`. CLI strips them automatically when `--model model-armor`.
- ID field returned by backend is `guardrails_config_id`, not `_id`. CLI normalizes both internally so `--id-only` and `get <id>` work as expected.

## Document AI Commands

A Document AI agent extracts structured JSON from unstructured documents (PDFs, images, scanned forms). Each agent has a **schema** defining fields to extract, **instructions** guiding the LLM, and a **model + provider**. Backend stores them as AI Agents with `agent_type: "document_ai"`.

- `imbrace document-ai list [--search <q>] [--all] --json` — List Document AI agents. `--all` includes regular agents (default: `documentAiOnly`).
- `imbrace document-ai get <agentId> --json` — Show details + extraction schema.
- `imbrace document-ai create -n "X" -i "instructions" --model gpt-4o --schema '<json>' [--provider-id <uuid>] [--description] [--workflow-name] --json` — Create. Schema can also be loaded from a file via `--schema-file ./schema.json`.
- `imbrace document-ai update <agentId> [--name|--instructions|--model|--provider-id|--schema|--schema-file|--description|--workflow-name] --json` — Partial update.
- `imbrace document-ai delete <agentId> --yes --json`
- `imbrace document-ai process --url <url> --org-id <orgId> [--agent-id <id>] [--model] [--instructions] [--board-id] [--language] [--additional-instructions] [--chunk-size] [--max-concurrent] [--max-retries] [--no-enhanced-processing] --json` — Run extraction on a PDF/image URL. Either `--agent-id` or `--model` is required.
- `imbrace document-ai suggest-schema --url <url> --org-id <orgId> [--model] --json` — Ask the LLM to inspect a sample document and propose a schema.

Schema example:
```json
{
  "invoice_number": { "type": "string", "description": "Invoice ID" },
  "total_amount":   { "type": "number" },
  "due_date":       { "type": "string", "format": "date" }
}
```

## Workflow Commands

A workflow (powered by Activepieces) is a chain of nodes: a trigger fires, then actions run in sequence. Use it for automation — e.g. "when Slack message arrives → ask AI → reply to thread".

**Flow CRUD:**

- `imbrace workflow list --json` — List all workflows and their IDs
- `imbrace workflow list --folder-id <folderId> --json` — Filter to flows in one folder/category
- `imbrace workflow list --folder-id NULL --json` — Only flows that aren't in any folder
- `imbrace workflow get <flowId> --json` — Get details (including node tree)
- `imbrace workflow create --name "My Flow" --json` — Create an empty workflow
- `imbrace workflow create --name "X" --folder-id <folderId> --json` — Create directly inside a category folder
- `imbrace workflow move <flowId> --folder-id <folderId>` — Move a flow into a folder. Pass `NULL` to unfile.
- `imbrace workflow delete <flowId> --yes --json` — Delete a workflow

**Discover available integrations (pieces):**

- `imbrace workflow piece list --json` — List all 126 integrations (Slack, Gmail, OpenAI, ...)
- `imbrace workflow piece list --search slack --json` — Search integrations
- `imbrace workflow piece detail <pieceName> --json` — Get full schema (actions, triggers, input fields)

**Build node tree:**

- `imbrace workflow node list <flowId> --json` — Show the trigger + action chain
- `imbrace workflow node add <flowId> --type trigger --piece <name> --trigger-name <id> --input '{...}' --json` — Add trigger
- `imbrace workflow node add <flowId> --type action --piece <name> --action-name <id> --after <parent> --input '{...}' --json` — Add action
- `imbrace workflow node update <flowId> <nodeName> --input '{...}' --json` — Update node input
- `imbrace workflow node delete <flowId> <nodeName> --yes --json` — Remove a node
- `imbrace workflow node add-raw <flowId> --op-file <path>` — Apply raw flow operation. Use for advanced node types (`ROUTER`, `LOOP_ON_ITEMS`, `CODE`) that `node add` doesn't expose. Body shape: `{ type: "ADD_ACTION" \| "UPDATE_ACTION" \| "UPDATE_TRIGGER" \| "DELETE_ACTION", request: {...} }`. Also accepts `--op '<inline JSON>'` or `--stdin`.

**Run history:**

- `imbrace workflow runs --json` — List recent runs
- `imbrace workflow run-detail <runId> --json` — Get run details (status, failed step, logs)

**Connections (OAuth / API keys for external services):**

- `imbrace workflow conn list --json` — List saved connections
- `imbrace workflow conn get <connId> --json` — Get details of a single connection
- `imbrace workflow conn create --piece <name> --type <T> --value "<token>" --json` — Save credential. `--type`: `SECRET_TEXT` | `OAUTH2` | `CLOUD_OAUTH2` | `BASIC_AUTH` | `CUSTOM_AUTH`
- `imbrace workflow conn delete <connId> --yes` — Delete a connection

**Folders (organize flows — UI calls them "Categories"):**

- `imbrace workflow folder list --json` — List all folders
- `imbrace workflow folder get <folderId> --json` — Folder details
- `imbrace workflow folder create --name "Sales" --json` — Create (auto-resolves projectId)
- `imbrace workflow folder update <folderId> --name "Renamed" --json` — Rename
- `imbrace workflow folder delete <folderId> --yes` — Delete (flows inside become unfiled, NOT deleted)

The platform auto-creates 4 system folders that show up as Categories in the UI. Use `workflow folder list` to discover their IDs:

| UI Category | Backend folder name | Purpose |
|---|---|---|
| `Channel Workflow` | `Channel Workflow` | Messaging / channel automation |
| `Board Automation` | `Board Automation` | Triggered by data-board events; flows usually use the `automate-data-board` action with a `boardId` |
| `AI Agent Skills` | `AI Agent Capabilities` | Skills callable by AI agents |
| `Others` | `Others` | Default catch-all |

To make a flow appear in a UI Category, place it in the matching system folder via `workflow create --folder-id <id>` or `workflow move <flowId> --folder-id <id>`. Flows created with no folder land in "All flows" only (not in any Category).

**MCP servers (Model Context Protocol — let AI agents call piece tools):**

- `imbrace workflow mcp list --json` — List MCP servers for the project
- `imbrace workflow mcp get <mcpId> --json` — Server details (token NOT shown for security)
- `imbrace workflow mcp create --name "My MCP" --json` — Create. **Token is shown once at creation — save it immediately.**
- `imbrace workflow mcp rotate-token <mcpId> --yes` — Issue a new token. Old token stops working immediately.
- `imbrace workflow mcp delete <mcpId> --yes` — Delete the server

**Lifecycle:**

- `imbrace workflow publish <flowId>` — Lock current draft as published version (REQUIRED before enable)
- `imbrace workflow enable <flowId>` — Enable auto-trigger
- `imbrace workflow disable <flowId>` — Stop auto-trigger (keeps published version)

**Trigger flow manually:**

- `imbrace workflow run <flowId> --payload '{...}' --json` — Trigger async (fire-and-forget)
- `imbrace workflow run <flowId> --payload '{...}' --sync --json` — Trigger and wait. ⚠️ Sync polling may time out at ~30s even when the run finishes faster — fall back to `imbrace workflow runs` + `run-detail <runId>` to verify.

**Order matters:** create flow → add nodes → **publish** → **enable** → run. Trying to `enable` before `publish` returns 500 "publishedFlowVersionId is required".

### Workflow flags

**Flow-level:**
- `--name` / `-n` — Workflow display name (required for create)
- `--folder-id` — Folder to place flow in
- `--yes` / `-y` — Skip confirmation on delete
- `--json` — Machine-readable output

**Node-level:**
- `--type` — `trigger` or `action`
- `--piece` — Piece name (e.g. `@activepieces/piece-slack`)
- `--trigger-name` — Trigger identifier from piece (when `--type trigger`)
- `--action-name` — Action identifier from piece (when `--type action`)
- `--after` — Place node after this step name (default: append at end)
- `--input` — JSON string with field values (use `{{trigger.X}}` or `{{step_N.output.Y}}` for variables)
- `--connection` — Connection ID to authenticate the piece

**Run flags:**
- `--payload` — JSON payload to send (becomes `trigger.body` in flow)
- `--sync` — Wait for flow to finish (timeout 60s); without it, fire-and-forget
- `--limit` — Max runs to list (default 10)

### How Workflow create works (under the hood)

`imbrace workflow create` calls `client.activepieces.createFlow({ displayName, projectId })`. The route auto-discovers `projectId` from existing flows so the user doesn't pass it.

Adding/editing nodes calls `client.activepieces.applyFlowOperation(flowId, { type, request })` with operation types:
- `UPDATE_TRIGGER` — replace trigger node
- `ADD_ACTION` — add an action after a parent step
- `UPDATE_ACTION` — change input of an existing action
- `DELETE_ACTION` — remove a node and re-link parent → child
- `LOCK_AND_PUBLISH` — finalize current version (used by `workflow publish`)
- `CHANGE_STATUS` — toggle ENABLED/DISABLED (used by `workflow enable/disable`)

Piece schemas (`piece detail`) come from a raw `GET /activepieces/v1/pieces/<pieceName>` because SDK's `listPieces()` only returns counts (`actions: 25`, `triggers: 12`), not field schemas.

### Flow JSON anatomy

A workflow stores its node tree under `version.trigger`, with `nextAction` linking the chain:

```ts
{
  id: "06oWoBQpL...",
  status: "ENABLED" | "DISABLED",
  version: {
    displayName: "My Flow",
    trigger: {
      name: "trigger",
      type: "PIECE_TRIGGER",  // or BRANCH, LOOP_ON_ITEMS, CODE
      settings: { pieceName, triggerName, input },
      nextAction: {
        name: "step_1",
        type: "PIECE",
        settings: { pieceName, actionName, input },
        nextAction: { name: "step_2", ... }
      }
    }
  }
}
```

### Variable syntax inside node `input`

- `{{trigger.body.X}}` — field X from trigger payload
- `{{trigger.X}}` — top-level trigger field (for piece triggers)
- `{{step_1.output.Y}}` — output field Y from step_1
- `{{connections.<connId>.access_token}}` — connection field

### Workflow anatomy — 6 layers

```
Project
  └─ Flow (container — id, name, status: ENABLED/DISABLED)
       └─ Version (snapshot — DRAFT or LOCKED, contains the node tree)
            └─ Nodes (cây trigger + actions, linked via nextAction)
                 ├─ trigger (always 1, type PIECE_TRIGGER or EMPTY)
                 └─ step_1, step_2, ... (0..N actions)
       └─ Runs (execution history — status, logs per run)

Cross-flow shared:
  ├─ Connections (OAuth / API keys, reused across flows)
  └─ Pieces (catalog of 126 integrations: Slack, Gmail, OpenAI, ...)
```

### Node types

A node's `type` field determines its role in the flow:

| Type | Role | CLI support |
|---|---|---|
| `PIECE_TRIGGER` | "When does the flow run" — Slack message, webhook, cron, ... | ✅ `node add --type trigger` |
| `PIECE` | "What runs after" — send Slack, ask AI, HTTP request, ... | ✅ `node add --type action` |
| `EMPTY` | Placeholder before a real trigger is set (default after `workflow create`) | ✅ Read-only |
| `ROUTER` | Multi-condition switch (replaces legacy BRANCH) | ✅ `node add-raw` |
| `LOOP_ON_ITEMS` | Loop over an array | ✅ `node add-raw` |
| `CODE` | Inline JavaScript | ✅ `node add-raw` |

Most flows use only `PIECE_TRIGGER` + `PIECE`. For the other 3 types use `node add-raw` with a full Activepieces operation payload — see shapes below.

**Constraints when composing advanced nodes:**
1. `LOOP_ON_ITEMS` and `CODE` must have a `parentStep` of type `trigger` or `PIECE`. Setting parent to a `ROUTER` (or another LOOP) returns 400 `Router step parent undefined not found`. To put a LOOP/CODE inside a router branch, build the inner action as part of the ROUTER's `children` array, not as a separate ADD_ACTION.
2. `node list` only walks the linear `trigger → nextAction → nextAction` chain. Children of `ROUTER` (in `children: []`) and the body of `LOOP_ON_ITEMS` (in `firstLoopAction`) are stored server-side but not displayed by the flat list. Use `workflow get <flowId> --json` to see the full nested tree.

**Gotchas when updating live flows:**
- **Flow lock** — if the user has the flow open in their browser, CLI updates may be rejected with `"edited at the last minute, try again later"` or silently overwritten when the UI auto-saves. Ask the user to close the flow editor tab before bulk-updating via CLI; verify with `workflow get --json` that your changes stuck.
- **AI Connector `assistant-request` prompt field** — the `prompt` input is a NESTED object, not a plain string. The UI form binds to `input.prompt.prompt`. Plain string values like `input.prompt = "text"` are accepted by the API but the UI renders the field as empty. Always send `{ "prompt": { "prompt": "actual text" } }`. The separate `custom_instructions` field IS a plain string and maps to the UI's "Custom Instructions" section.

### Raw operation shapes for advanced nodes

For `ROUTER`, `LOOP_ON_ITEMS`, `CODE` use `imbrace workflow node add-raw <flowId> --op-file <path>`. The op file contains `{ type, request }` — the full Activepieces flow-operation body. Examples:

**ROUTER** — multi-condition switch:
```json
{
  "type": "ADD_ACTION",
  "request": {
    "parentStep": "trigger",
    "action": {
      "name": "step_1",
      "type": "ROUTER",
      "displayName": "Check priority",
      "settings": {
        "branches": [
          {
            "branchName": "High",
            "branchType": "CONDITION",
            "conditions": [[{
              "operator": "TEXT_EXACTLY_MATCHES",
              "firstValue": "{{trigger.body.priority}}",
              "secondValue": "high",
              "caseSensitive": false
            }]]
          },
          { "branchName": "Default", "branchType": "FALLBACK" }
        ],
        "executionType": "EXECUTE_FIRST_MATCH",
        "sampleData": {}
      },
      "children": [null, null],
      "valid": true
    }
  }
}
```

**LOOP_ON_ITEMS** — loop over an array. `parentStep` must be a `trigger` or `PIECE` (not a ROUTER):
```json
{
  "type": "ADD_ACTION",
  "request": {
    "parentStep": "trigger",
    "action": {
      "name": "step_1",
      "type": "LOOP_ON_ITEMS",
      "displayName": "Loop users",
      "settings": { "items": "{{trigger.body.users}}" },
      "firstLoopAction": null,
      "valid": true
    }
  }
}
```

**CODE** — inline JavaScript:
```json
{
  "type": "ADD_ACTION",
  "request": {
    "parentStep": "step_1",
    "action": {
      "name": "step_2",
      "type": "CODE",
      "displayName": "Transform",
      "settings": {
        "input": { "x": "{{trigger.body.value}}" },
        "sourceCode": {
          "code": "export const code = async (inputs) => ({ doubled: inputs.x * 2 });",
          "packageJson": "{\"dependencies\":{}}"
        }
      },
      "valid": true
    }
  }
}
```

**Common operation types** for `add-raw`:
- `ADD_ACTION` — add a new node (with `parentStep` + `action` shape above)
- `UPDATE_ACTION` — replace an existing node's settings (request: `{ name, type, displayName, settings, valid }`)
- `UPDATE_TRIGGER` — replace the trigger node
- `DELETE_ACTION` — remove a node (request: `{ names: ["step_X"] }`)

### Connection types

When calling `workflow conn create --type <X>` (Sprint 3), valid types are:

| Type | Use case |
|---|---|
| `SECRET_TEXT` | Simple API key (Slack bot token, OpenAI key) |
| `OAUTH2` | OAuth flow with custom app (Google, Slack OAuth) |
| `CLOUD_OAUTH2` | OAuth via Activepieces hosted apps (no custom app setup needed) |
| `BASIC_AUTH` | username + password |
| `CUSTOM_AUTH` | Custom format defined by the piece |

Use `imbrace workflow piece detail <pieceName>` to see which auth type a piece accepts.

### Workflow lifecycle

```
DRAFT (editing) → publish → LOCKED (production) → enable → ENABLED (auto-running)
                                                ↑
                                                disable (back to DISABLED but still LOCKED)
```

- `workflow publish` locks the current draft version and marks it as the production version.
- `workflow enable` turns on auto-trigger — fires the flow whenever its trigger condition is met.
- `workflow disable` stops auto-trigger but keeps the published version intact.
- Edit after publish: backend creates a new DRAFT version; previous LOCKED version keeps running until you publish the new draft.

## Setup

```bash
# 1. Start the API
cd api && bun install && bun run dev

# 2. Install the CLI (one-shot — handles build, link, and cross-shell PATH)
cd .. && ./install.sh

# 3. Login
imbrace login --api-key api_xxx...

# 4. Verify
imbrace whoami --json
```

The `install.sh` script symlinks `imbrace` into `/opt/homebrew/bin` (Apple Silicon) or `/usr/local/bin` so the command works even from shells that don't load nvm — for example conda's `(base)` env.

## Quick Start for Coding Agents

### Data Board
```bash
# Get available boards
imbrace data-board list --json

# Create a board
imbrace data-board create --name "Leads" --json

# Add a field
imbrace data-board create-field <boardId> --name "Company" --type ShortText --json

# Add a record (Name field is required — use its field ID)
imbrace data-board create-item <boardId> --fields '[{"board_field_id":"<nameFieldId>","value":"John Doe"},{"board_field_id":"<fieldId>","value":"Acme Corp"}]' --json

# List records
imbrace data-board list-items --board-id <boardId> --json

# Export to CSV
imbrace data-board export-csv --board-id <boardId> --out ./leads.csv
```

### AI Agent
```bash
# Discover what's available
imbrace ai-agent list --json
imbrace ai-agent list-providers --json
imbrace ai-agent list-models --provider-id system --json
imbrace ai-agent list-folders --json

# Create an agent (minimal)
imbrace ai-agent create --name "Sales Bot" --instructions "You are a sales assistant" --json

# Create with custom provider + Knowledge Hub folder
imbrace ai-agent create --name "RAG Bot" \
  --instructions "Answer using the provided knowledge" \
  --provider-id e2629292-7e9f-4d55-ba18-6827747eab33 \
  --model gpt-4o-mini \
  --folder-ids 69bb82faa2cc764639bc6bdb \
  --json

# Update — partial (other fields preserved)
imbrace ai-agent update <agentId> --instructions "Updated" --temperature 0.7 --json

# Delete
imbrace ai-agent delete <agentId> --yes --json
```

### Workflow — build a Slack auto-reply bot end-to-end

**Note:** Steps 1, 3, 4, 5, 6, 7 use Sprint 1+2 commands (already implemented).
Steps 2, 8, 9 use Sprint 3 commands (**not yet implemented** — for now, do them via the [UI builder](https://cloud.imbrace.co/workflow-v2)).

```bash
# 1. Discover Slack + AI integrations (schemas)  ✅ Sprint 2
imbrace workflow piece list --search slack --json
imbrace workflow piece detail @activepieces/piece-slack --json
imbrace workflow piece detail @activepieces/piece-ai-connector --json

# 2. Save Slack token (one-time per workspace)  ✅ Sprint 3
imbrace workflow conn create \
  --piece @activepieces/piece-slack \
  --type SECRET_TEXT \
  --value "xoxb-actual-token" \
  --json
# → note conn._id

# 3. Create empty flow  ✅ Sprint 1
imbrace workflow create --name "Slack AI Reply Bot" --json
# → note flow._id

# 4. Add Slack trigger (catch new messages)  ✅ Sprint 2
imbrace workflow node add <flowId> \
  --type trigger \
  --piece @activepieces/piece-slack \
  --trigger-name new_message_in_channel \
  --connection <connId> \
  --input '{"channel":"C0XXXXX"}' \
  --json

# 5. Add AI action (draft a reply)  ✅ Sprint 2
imbrace workflow node add <flowId> \
  --type action \
  --piece @activepieces/piece-ai-connector \
  --action-name ask \
  --after trigger \
  --input '{"modelName":"gpt-4o","prompt":"Draft a friendly reply to: {{trigger.text}}"}' \
  --json

# 6. Add Slack reply (post answer back to thread)  ✅ Sprint 2
imbrace workflow node add <flowId> \
  --type action \
  --piece @activepieces/piece-slack \
  --action-name send_channel_message \
  --after step_1 \
  --connection <connId> \
  --input '{"channel":"{{trigger.channel}}","text":"{{step_1.output.text}}","thread_ts":"{{trigger.ts}}"}' \
  --json

# 7. Verify the node tree  ✅ Sprint 2
imbrace workflow node list <flowId> --json

# 8. Test run with sample payload  ✅ Sprint 3
#    --sync may time out (~30s) even if the run finishes faster.
#    If it times out, use 'workflow runs' + 'run-detail' to fetch the result.
imbrace workflow run <flowId> --sync \
  --payload '{"text":"How do I reset my password?","channel":"C0XXXXX","ts":"1234.5"}' \
  --json

# 9. Publish & enable so it runs automatically  ✅ Sprint 3
imbrace workflow publish <flowId> --json
imbrace workflow enable <flowId> --json
```

## Adding a New Service

To add a new service (e.g. `workflow`):

1. Create route in `api/src/routes/workflow.ts`
2. Mount in `api/src/index.ts` with `authMiddleware`
3. Create CLI commands in `cli/src/commands/workflow/`
4. Add topic in `cli/package.json` under `oclif.topics`
