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

# Workflows

> HTTP automation scripts that agents can discover and trigger to call external APIs on behalf of your SOC.

Workflows are **HTTP automation scripts** that call external APIs on behalf of your SOC. Python is the glue layer — you use it to construct HTTP requests, call endpoints via `ctx.http`, and parse responses. Agents discover workflows via API or MCP and trigger them during investigations.

Calseta supports two workflow types: **sandboxed HTTP automation scripts** for logic that runs inside the platform, and **custom HTTP endpoints** for triggering external automation.

## HTTP Automation Scripts

Each workflow is an `async def run()` function that receives a `WorkflowContext` and returns a `WorkflowResult`. The primary tool is `ctx.http` — an `httpx.AsyncClient` for calling any REST API, webhook, serverless function, or HTTP endpoint:

```python theme={null}
async def run(ctx: WorkflowContext) -> WorkflowResult:
    api_key = ctx.secrets.get("FIREWALL_API_KEY")
    if not api_key:
        return WorkflowResult.fail("FIREWALL_API_KEY is not set")

    ip = ctx.indicator.value
    try:
        resp = await ctx.http.post(
            "https://firewall.internal/api/block",
            json={"ip": ip},
            headers={"Authorization": f"Bearer {api_key}"}
        )
    except Exception as exc:
        return WorkflowResult.fail(f"Request failed: {exc}")

    if resp.status_code == 200:
        return WorkflowResult.ok(
            message=f"Blocked {ip}",
            data={"status": resp.status_code}
        )
    return WorkflowResult.fail(
        message=f"Failed to block {ip}: {resp.status_code}"
    )
```

### What You Can Do

* **Call any HTTP endpoint** — REST APIs, webhooks, Lambda Function URLs, Logic App triggers, internal services
* **Use environment variables for secrets** — API keys, tokens, webhook URLs via `ctx.secrets.get("KEY")`
* **Use safe stdlib modules** — `json`, `hashlib`, `hmac`, `base64`, `datetime`, `re`, `uuid`, and more
* **Use builtin integration clients** — `ctx.integrations.okta` and `ctx.integrations.entra` for pre-built Okta/Entra identity actions

### WorkflowContext

The context object provides everything a workflow needs:

| Property       | Description                                                                    |
| -------------- | ------------------------------------------------------------------------------ |
| `indicator`    | The indicator being acted on (type, value, malice verdict, enrichment results) |
| `alert`        | The alert that triggered the workflow (may be None)                            |
| `http`         | Pre-configured `httpx.AsyncClient` for calling external HTTP endpoints         |
| `log`          | Structured logger for execution steps                                          |
| `secrets`      | Read environment variables: `ctx.secrets.get("KEY")` returns `str` or `None`   |
| `integrations` | Pre-built integration clients (Okta, Entra) — `None` if not configured         |

### WorkflowResult

Every workflow returns a result — never raises an exception:

```python theme={null}
WorkflowResult.ok(message="Success", data={"key": "value"})
WorkflowResult.fail(message="Failed because...")
```

### Code Safety

Workflow code is validated via AST analysis at save time:

* **Allowed:** Python standard library (`json`, `hashlib`, `hmac`, `base64`, `datetime`, `re`, `uuid`, etc.), `calseta.workflows`
* **Blocked:** Third-party packages, file system access, subprocess calls, `os`, `sys`, `socket`

Workflows interact with external systems exclusively through `ctx.http`.

### Builtin Workflows

Calseta ships with 9 pre-built workflows for Okta and Microsoft Entra identity lifecycle management. These are the "batteries included" version of the HTTP automation pattern — they use the same `ctx.http` under the hood, wrapped in typed integration clients:

* **Okta** — Revoke sessions, suspend/unsuspend user, reset password, force password expiry
* **Entra** — Revoke sign-in sessions, disable/enable account, force MFA re-registration

All builtins require human approval when triggered by an AI agent.

### Code Generation

Use `POST /v1/workflows/generate` to describe what you want in plain English. The API generates valid workflow code from your description, validates it, and returns it for review before saving. Requires `ANTHROPIC_API_KEY`.

### Testing

Use `POST /v1/workflows/{uuid}/test` to run a workflow with mock HTTP responses. No real external calls are made — the test endpoint intercepts HTTP requests and returns your mock responses.

## Custom HTTP Endpoint Workflows

For automation that lives outside Calseta — Azure Logic Apps, AWS Lambda functions, n8n workflows, or any HTTP-accessible service — create an HTTP endpoint workflow. Calseta calls your endpoint with the alert and indicator context, and your service performs the action.

This pattern is useful for:

* Triggering Logic Apps that interact with internal systems
* Calling Lambda functions that access cloud-specific resources
* Integrating with ticketing systems, chat platforms, or custom tooling
* Any automation that needs dependencies beyond Python's standard library

The workflow `documentation` field describes what the endpoint does, so agents can discover and reason about it alongside sandboxed workflows.

## Workflow States

| State      | Description                           |
| ---------- | ------------------------------------- |
| `draft`    | Under development, cannot be executed |
| `active`   | Available for execution               |
| `inactive` | Disabled, cannot be executed          |

## Human-in-the-Loop Approval

Workflows support a configurable approval gate via `approval_mode`:

* `"always"` — all triggers (human and agent) require approval before execution
* `"agent_only"` — only agent-triggered executions require approval; humans bypass the gate
* `"never"` — no approval required, execute immediately

```json theme={null}
{
  "approval_mode": "always",
  "approval_channel": "#soc-approvals",
  "approval_timeout_seconds": 3600,
  "risk_level": "high"
}
```

### Approval Flow

1. A request hits `POST /v1/workflows/{uuid}/execute`. The `trigger_source` is derived server-side from the API key's `key_type` (`human` or `agent`) — it is not a request body field.
2. If the trigger requires approval (based on `approval_mode` and `trigger_source`), Calseta creates an approval request. Agent keys (`key_type: agent`) must include `reason` and `confidence` in the request body.
3. Calseta notifies the configured channel with a unique `decide_token`
4. A human approves or rejects via the browser approval page, Slack buttons, or REST API
5. If approved, the workflow is queued for execution
6. If rejected or expired, no action is taken

### Browser Approval Page

Every approval request includes a token-authenticated browser link that approvers can open directly — no API key or login required. The link is included in Slack and Teams notifications as a "Review & Decide" button.

The page displays the workflow name, risk level, indicator, agent reason, confidence, and expiry. Approvers click **Approve** or **Reject** and see an immediate confirmation.

The token is 256-bit entropy (`secrets.token_urlsafe(32)`), validated with constant-time comparison, single-use (consumed on decision), and time-bound (same expiry as the approval request). Use HTTPS in production since the token is in the URL.

### Notification Channels

| Channel | Interactive                                 | Configuration                      |
| ------- | ------------------------------------------- | ---------------------------------- |
| Slack   | Yes (approve/reject buttons + browser link) | `APPROVAL_NOTIFIER=slack`          |
| Teams   | Yes (browser approval link)                 | `APPROVAL_NOTIFIER=teams`          |
| None    | —                                           | `APPROVAL_NOTIFIER=none` (default) |

## Version History

Every code change increments `code_version` and creates a version history entry. Workflow runs record which version was executed, providing a complete audit trail.

## API Endpoints

| Method  | Endpoint                                | Description             |
| ------- | --------------------------------------- | ----------------------- |
| `GET`   | `/v1/workflows`                         | List workflows          |
| `GET`   | `/v1/workflows/{uuid}`                  | Get a workflow          |
| `POST`  | `/v1/workflows`                         | Create a workflow       |
| `PATCH` | `/v1/workflows/{uuid}`                  | Update a workflow       |
| `POST`  | `/v1/workflows/{uuid}/execute`          | Execute a workflow      |
| `GET`   | `/v1/workflows/{uuid}/runs`             | List runs               |
| `POST`  | `/v1/workflow-approvals/{uuid}/approve` | Approve                 |
| `POST`  | `/v1/workflow-approvals/{uuid}/reject`  | Reject                  |
| `GET`   | `/v1/approvals/{uuid}/decide?token=...` | Browser approval page   |
| `POST`  | `/v1/approvals/{uuid}/decide`           | Submit browser decision |

## MCP Access

* **Resource:** `calseta://workflows` — list active workflows
* **Resource:** `calseta://workflows/{uuid}` — get a workflow
* **Tool:** `execute_workflow` — trigger execution (always agent-triggered)
