> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trymaitai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflow logging (Bring Your Own Workflow)

> Log externally executed workflows into Maitai with a 1–2 line Python SDK, raw HTTP, or n8n — same Portal Runs experience as native workflows.

Already running workflows outside Maitai (Python pipelines, n8n, Zapier, Make) and want them indexed in the Portal? Use **Bring Your Own Workflow (BYOW)** to ship completed runs to Maitai with minimal integration cost.

<Info>
  Capture is fire-and-forget and engineered to **never raise into your application**. Respects `MAITAI_LOGGING_ENABLED` and `MAITAI_LOG_SAMPLE_RATE`. First log with an unknown ref auto-creates a shell workflow — no pre-registration.
</Info>

## Python SDK

### Context manager

```python theme={null}
import maitai

with maitai.workflow_run(
    "order-pipeline",
    application="checkout-app",
    input={"order_id": 1},
) as run:
    # your workflow logic…
    result = {"ok": True}
    run.output = result
```

Nested `maitai.log(...)` / proxied LLM calls automatically inherit the run's `session_id`, so the cost card and View Session populate without extra wiring.

### Decorator

```python theme={null}
import maitai

@maitai.workflow("order-pipeline", application="checkout-app")
def handle(order_id: int):
    return {"ok": True, "order_id": order_id}
```

### One-shot

```python theme={null}
import maitai

maitai.log_workflow_run(
    workflow_ref="order-pipeline",
    application="checkout-app",
    session_id="run-123",
    status="COMPLETED",
    input={"order_id": 1},
    output={"ok": True},
)
```

### LangGraph / observe as workflow

When instrumenting with `maitai.observe()`, declare which graphs are workflows so they persist as `workflow_run` rows instead of observed agents:

```python theme={null}
import maitai

maitai.observe(
    application="checkout-app",
    workflows=["order-graph"],  # stamps maitai.run_type=workflow
)
# or force every span:
# maitai.observe(application="checkout-app", run_type="workflow")
```

## Raw HTTP

```bash theme={null}
curl -X PUT "https://api.trymaitai.ai/workflow/runs/log" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $MAITAI_API_KEY" \
  -d '{
    "workflow_ref": "order-pipeline",
    "application": "checkout-app",
    "session_id": "run-123",
    "status": "COMPLETED",
    "input": {"order_id": 1},
    "output": {"ok": true},
    "source_tool": "http"
  }'
```

Idempotent on `session_id`. Prefer a **write-only ingest token** (`WORKFLOW_INGEST`) from Portal → Forge → Workflows → **Connect external** when pasting into third-party SaaS.

## n8n

1. Open Portal → Forge → Workflows → **Connect external**.
2. Pick **n8n**, choose application + workflow ref, mint an ingest token.
3. Add an HTTP Request node as the **last** step, or import the starter workflow from the repo (`docs/external/sdk/n8n/maitai-byow-log.json`) and replace placeholders:
   * Method: `PUT`
   * URL: `https://api.trymaitai.ai/workflow/runs/log` (or your local `http://host.docker.internal:5000/workflow/runs/log`)
   * Header `x-api-key`: your ingest token
   * Body (JSON). In the Portal wizard artifact, n8n uses expression fields with an `=` prefix.
     **Never** send bare `$execution.id` as `session_id` (values like `"1"` collide with other traffic and cost backfill). Namespace it:
     ```json theme={null}
     {
       "workflow_ref": "order-pipeline",
       "application": "checkout-app",
       "session_id": "={{ 'n8n-' + $workflow.id + '-' + $execution.id }}",
       "status": "COMPLETED",
       "input": "={{$json}}",
       "output": "={{$json}}",
       "source_tool": "n8n"
     }
     ```
     The API rejects numeric-only / short (`< 8` char) external `session_id` values.
4. Optional: Error Workflow HTTP node with `"status": "FAILED"` and `error_message`.
5. Use **Wait for first run** in the Portal wizard — it navigates to the rendered run when it lands.

### Local verify (Docker)

```bash theme={null}
docker run -it --rm \
  -p 5678:5678 \
  -e WEBHOOK_URL=http://localhost:5678/ \
  -e GENERIC_TIMEZONE=UTC \
  n8nio/n8n
```

Open [http://localhost:5678](http://localhost:5678) → **Import from File** → `docs/external/sdk/n8n/maitai-byow-log.json` → set `x-api-key`, `workflow_ref`, and `application` → Execute. Confirm the run appears under the workflow in Portal (or via the wizard listener). From Docker Desktop on Mac/Windows, `host.docker.internal` reaches a backend on the host at `:5000`.

## TypeScript SDK

```ts theme={null}
import { logWorkflowRun } from "maitai";

await logWorkflowRun({
  workflow_ref: "order-pipeline",
  application: "checkout-app",
  session_id: "run-123",
  status: "COMPLETED",
  input: { order_id: 1 },
  output: { ok: true },
  source_tool: "node",
});
```

## What appears in the Portal

External runs show under the linked application and workflow with `source=EXTERNAL`. Input/output panels, cost (when LLM calls share the session), and execution timeline (when a profile/spans are sent) match native runs. Flow-tab graph import for external definitions is a follow-up.
