# Deployment

Once a workflow runs cleanly in the editor, you expose it to the outside world from its **Production space**: left sidebar → **Production** → your agent → **Deploy**. Three surfaces, same workflow:

| Surface | Caller | Auth |
| ------- | ------ | ---- |
| **API** | Your backend, scripts, third parties | Per-key `X-Agent-Key` header |
| **Widget** | A browser, embedded in your website | Anonymous, scoped per session |
| **Shared chat** | Anyone with the URL | Optional username + password |

Each surface is independently toggled — enable only what you need. The agent must also be **active** (the Live/Draft toggle on the Overview or Settings tab) for any surface to serve traffic — see [Production](#/docs/production).

## API

REST endpoint your code can POST to. One workflow can have many keys; revoke them individually.

### Enable

1. Open the agent's production space → **Deploy** → **API**.
2. Toggle **Enable published API**.
3. Click **Generate key**. Copy the secret *now* — only the hash is stored, you can't read it again. The page also builds a ready-made `curl` example with your key.

### Endpoint

```
POST /api/v1/published/{workflow_public_id}/chat
```

### Headers

- `X-Agent-Key: <your-key>` *(or)* `Authorization: Bearer <your-key>`
- `Content-Type: application/json`

### Body

```json
{
  "input":      "Summarize today's support tickets",
  "session_id": "optional-stable-id",
  "messages":   [{ "role": "user", "content": "..." }],
  "metadata":   { "anything": "you want" }
}
```

Provide `input` *or* a non-empty `messages` array. The last user message wins if both are present.

### Example

```bash
curl -X POST https://app.systalink.io/api/v1/published/wf_abc123/chat \
  -H 'X-Agent-Key: sk_live_xxx' \
  -H 'Content-Type: application/json' \
  -d '{"input":"Hello agent"}'
```

### Response

```json
{
  "workflow_id": "wf_abc123",
  "session_id":  "...",
  "result":      "Hi! How can I help?",
  "messages":    [ ...full transcript... ],
  "status":      "completed"
}
```

### Execution modes

The default call above is **synchronous**: the HTTP connection stays open until the workflow finishes. That is ideal for short workflows, but not for anything that can exceed ~30s — clients and proxies time out. For long-running workflows, use **async + SSE/polling** (stream progress or pull the result) or a **callback** (the server pushes it to an endpoint you expose).

| Mode | When | How |
| ---- | ---- | --- |
| **Sync** (default) | Fast calls (< ~30s) | `POST .../chat` → `200` with the full result |
| **Async + SSE/polling** | Long workflows, no public callback endpoint | `POST` with `"async": true` → `202` + `run_id`, then stream or poll the run |
| **Callback (push)** | Long workflows, you expose an endpoint | `POST` with `callback_url` → server POSTs the result to you |

#### Async

Add `"async": true` to the body. The endpoint returns **202** immediately and the workflow runs in the background:

```json
{
  "run_id":   "run_abc123",
  "status":   "pending",
  "poll_url": "/api/v1/published/{workflow_id}/runs/run_abc123",
  "stream_url": "/api/v1/published/{workflow_id}/runs/run_abc123/stream"
}
```

#### Polling

`GET` the run with the same `X-Agent-Key` header until `status` is terminal:

```
GET /api/v1/published/{workflow_id}/runs/{run_id}
```

```json
{
  "run_id":      "run_abc123",
  "status":      "completed",
  "result":      "...",
  "error":       null,
  "created_at":  "...",
  "finished_at": "..."
}
```

`status` is one of `pending`, `running`, `completed`, `failed`, `awaiting_approval`. Poll until `completed` or `failed`.

#### SSE stream

Use the returned `stream_url` to follow public progress events in real time:

```
GET /api/v1/published/{workflow_id}/runs/{run_id}/stream
Accept: text/event-stream
X-Agent-Key: <your-key>
```

```bash
curl -N https://app.systalink.io/api/v1/published/wf_abc123/runs/run_abc123/stream \
  -H 'X-Agent-Key: sk_live_xxx' \
  -H 'Accept: text/event-stream'
```

The stream sends data-only SSE frames. Ignore comment frames such as `: keepalive`, parse each `data:` payload as JSON, and stop on a terminal workflow event or `stream_done`.

| Event type | Public fields |
| ---------- | ------------- |
| `workflow_started`, `workflow_resumed`, `workflow_retried` | `run_id`, `workflow_id`, `node_count`, `edge_count`, `start_nodes`, optional `resume_from_node` / `retry_from_node` |
| `node_started` | `run_id`, `node_id`, `node_type`, redacted `label`, `status: "running"` |
| `node_completed`, `node_failed` | `run_id`, `node_id`, `node_type`, redacted `label`, `attempts`, `status`, optional redacted `error` |
| `edge_traversed` | `run_id`, `source`, `target` |
| `workflow_completed` | `run_id`, `workflow_id`, `status: "completed"`, `result` |
| `workflow_awaiting_approval` | `run_id`, `workflow_id`, `status: "awaiting_approval"` |
| `workflow_cancelled`, `workflow_failed` | `run_id`, `workflow_id`, terminal `status`, optional `error` |
| `stream_done` | `run_id`; may be sent after a terminal replay |

Browser `EventSource` cannot attach custom auth headers. In browser clients, use `fetch` with a readable stream (or the frontend helper `followPublishedRunStream` from `src/api/published.js`); in server-side clients, keep using `X-Agent-Key` or `Authorization: Bearer <key>`. Do **not** put agent keys in query strings.

Security boundary: the stream uses the same API-key check and workflow/run ownership check as polling. Invalid keys return `401`; a run from another workflow returns `404`. Public events are narrowed before they leave the API: node `input`, node `output`, full execution context, `input_data`, callback secrets, environment secrets, credentials, metadata, and crash/resume snapshots are not exposed. Labels and error strings are redacted/truncated before they leave the API.

#### Callback (push)

Add `callback_url` (and optionally `callback_secret`) to the chat body. The run executes in the background and, on completion, the server POSTs to your URL:

```json
{
  "input":           "Summarize today's tickets",
  "callback_url":    "https://your-app.example.com/agent-callback",
  "callback_secret": "whsec_your_shared_secret"
}
```

The callback payload:

```json
{
  "run_id":      "run_abc123",
  "workflow_id": "wf_abc123",
  "status":      "completed",
  "result":      "...",
  "error":       null
}
```

If `callback_secret` is set, the request carries `X-Signature-256: sha256=<hmac-sha256 of the raw body with your secret>` — verify it before trusting the payload.

### Rate limits

Per-key, configurable on the Deploy → API page. The platform also enforces a per-user execution quota — see [Troubleshooting](#troubleshooting) for the 429 case. Revoked keys return `401` immediately.

## Widget

A drop-in anonymous chat page for any website, embedded as an **iframe** — visitors don't sign in.

### Enable

1. **Deploy** → **Widget**.
2. Toggle **Enable widget**.
3. Configure:
   - **Widget title** and **welcome message**
   - **Allow images** / **Allow files** (tighten for unauthenticated traffic)
   - **Messages per session** (default 20) and **rate window** (default 3600 s)
   - **Theme** (`light` / `dark`)
4. Copy the **Iframe embed** snippet from the panel (the **Widget URL** is also shown for direct linking).

### Embed

```html
<iframe
  src="https://app.systalink.io/published/wf_abc123/widget"
  title="My agent"
  style="width:100%;min-height:760px;border:0;"
></iframe>
```

The widget page calls `POST /api/v1/published/{workflow_id}/widget/chat`. No key needed — the workflow must be widget-enabled or the endpoint returns `403`.

### Attachments

If **Allow files** is off but **Allow images** is on, the widget rejects non-image uploads with `400 "This published surface accepts image attachments only."` Max file size matches `MAX_UPLOAD_SIZE_MB` (server default 20 MB).

### Rate limits

Per session ID (cookie-based). When exceeded, the endpoint returns `429 "Rate limit reached for this widget session."` The bucket auto-clears after the configured window (default 3600 s).

## Shared chat

A standalone hosted chat URL — no embedding, no API call needed. Useful for internal tools or sharing a one-off agent with a client.

### Enable

1. **Deploy** → **Shared chat**.
2. Toggle **Enable shared chat**.
3. Configure:
   - **Share slug** (optional) — custom URL path. Defaults to the workflow's public id.
   - **Title** and **welcome message**
   - **Username** (optional) — if set, visitors must enter it on the login screen
   - **Password** (optional but recommended) — stored as a bcrypt hash; visitors enter it on login
   - **Allow images** / **Allow files**
   - **Messages per session** (default 40 / hour)

### URL

```
https://app.systalink.io/published/<slug-or-public-id>/chat
```

### Visitor history

Each visitor keeps their **conversation history per browser**: a sidebar on the chat page lists their previous sessions, and selecting one reloads the full transcript and resumes it. The same sessions appear (across all surfaces) in the agent's **Conversations** tab.

### Login flow

If a password is set, the page shows a login form. Submitting it calls:

```
POST /api/v1/published/{workflow_id}/share/login
{ "username": "...", "password": "..." }
```

On success the visitor gets a 12-hour bearer token, scoped to that single workflow. The token is then sent on every chat call:

```
POST /api/v1/published/{workflow_id}/share/chat
Authorization: Bearer <share-session-token>
```

Tokens are signed JWTs — no server-side session table.

### Attachments and rate limits

Same rules as the widget. Uploads go through:

```
POST /api/v1/published/{workflow_id}/attachments?surface=share
Authorization: Bearer <share-session-token>
```

## Machine-readable docs & API

The whole documentation is also published as static, no-auth, no-JavaScript files so an LLM or agent can fetch it directly (a `/docs/<topic>` SPA URL only returns the empty JS shell). Point tools at these URLs:

| URL | What it is |
| --- | ---------- |
| [`/llms.txt`](/llms.txt) | Concise index of every documented topic (llms.txt convention). |
| [`/llms-full.txt`](/llms-full.txt) | Every topic concatenated into one English file. |
| [`/llms-full-fr.txt`](/llms-full-fr.txt) | Every topic concatenated, in French. |
| [`/docs-md/en/<topic>.md`](/docs-md/en/overview.md) | Per-topic Markdown (also `/docs-md/fr/<topic>.md`). |
| [`/sitemap.xml`](/sitemap.xml) | Human pages plus both Markdown copies. |
| [`/robots.txt`](/robots.txt) | Crawl policy (documentation is public). |
| [`/api/v1/openapi.json`](/api/v1/openapi.json) | Machine-readable REST API schema for the published endpoints above. |

These files are regenerated from the same sources as the human docs, so they stay in sync automatically.

## Disabling

Toggle the surface off in **Deploy** (or all at once from **Settings → Surface authentication**). The relevant `/api/v1/published/...` endpoint immediately returns `403`. Existing API keys are *not* deleted — re-enabling the surface restores them. Use the explicit **Revoke** action on a key if you want it gone for good.

---

*Source: https://agentbuilder.systalink.sn/docs/deployment — human documentation.*
*Other language: [/docs-md/fr/deployment.md](/docs-md/fr/deployment.md).*
*Machine-readable index: [/llms.txt](/llms.txt).*
