# Abstra Hooks Guide

A **hook** is a workflow stage triggered by an inbound **HTTP request** (or an email). The stage's Python file runs top-to-bottom on each request: you read the request, do the work, optionally hand work off to downstream stages with `send_task`, and send an HTTP response. Use hooks to let the outside world trigger your workflow — webhooks, public API endpoints, callbacks, inbound email.

## Request/response API (`abstra.hooks`)

```python
from abstra.hooks import (
    get_request,        # parsed body
    get_raw_request,    # raw (unparsed) body
    get_email_request,  # when the hook is triggered by email
    send_json,
    send_response,
)
```

```python
get_request()      -> tuple[str | list | dict, dict[str, str], dict[str, str]]   # (body, query, headers)
get_raw_request()  -> tuple[str,                dict[str, str], dict[str, str]]   # (raw_body, query, headers)
get_email_request()-> MailParser   # .body, .subject, .from_, .to, .attachments

send_json(data: dict | list = {}, status_code: int = 200, headers: dict = {}) -> None
send_response(body: str | dict | list = "", status_code: int = 200, headers: dict = {}) -> None
```

- **Tuple order is `(body, query, headers)`** — body first. `query` and `headers` are `dict[str, str]`.
- `get_request()` **parses** the body by `Content-Type`: JSON → `dict`/`list`, form/multipart → `dict`. `get_raw_request()` returns the body as a raw `str` (multipart kept base64).
- `send_json` auto-sets `Content-Type: application/json`. `send_response` serializes non-string bodies to JSON; use it (with a `headers` `Content-Type`) for plain text / HTML / XML.
- Both raise `BadSendResponse` if `status_code` isn't an `int` or `headers` isn't a `dict`.
- An **uncaught exception ends the request as HTTP 500.** Wrap risky work in `try/except` and send a deliberate status.

## Endpoints & triggers

- URL shape: `https://<subdomain>.abstra.app/_hooks/<hook-path>`. Each hook has a **local** URL (your machine), a **remote dev** URL (anywhere, only while the dev server runs), and a **production** URL (after deploy).
- **Connector webhooks**: name the hook path `<connection-name>/<event-name>` (both kebab-case, e.g. `stripe/charge-succeeded`, `clicksign/upload`) and the hook automatically receives that connector's events; the event payload arrives as the request `body`.
- **Inbound email**: email `<subdomain>+<hook-path>@abstra-notifications.com` and read it with `get_email_request()`.

## Patterns

- **Acknowledge fast, work async.** Webhook senders retry/timeout aggressively. Validate, `send_json({"ok": True}, 200)` immediately, and offload heavy work to a tasklet via `send_task("type", payload)`. Don't do a 30s job inside the request.
- **Verify signatures on the RAW body.** HMAC signatures (Stripe, GitHub, …) are computed over the exact bytes. Use `get_raw_request()` — `get_request()` re-parses and would change the bytes. Compare with `hmac.compare_digest`.
- **Be idempotent.** Senders deliver the same event more than once. Dedupe on the event id (e.g. a unique column in a table) before acting.
- **Return the right status.** `2xx` tells the sender "got it" (stops retries); `4xx` for bad input you won't retry; `5xx` (or an uncaught exception) to ask the sender to retry.

## Inside a hook you also have the full stage SDK

`os.environ.get("NAME")` for env/secrets (values ≥10 chars are masked as `<MASKED>` in production logs) · `from abstra.logging import logger` · `import abstra.tables as at` (`insert`/`select`/`update`/`run_sql`) · `from abstra.connectors import run_connection_action` · `from abstra.tasks import send_task` · `from abstra.common import get_persistent_dir, get_execution_id` · `from abstra.ai import run_agent`.

## Footguns

- **Forgetting to send a response.** Always call `send_json`/`send_response` on every path (including error paths). Callers hang or time out otherwise.
- **Parsing the body before verifying its signature.** Verify on `get_raw_request()` bytes first, then parse.
- **Doing slow work inline.** Move it to a tasklet; the hook should return in well under the sender's timeout.
- **Trusting the payload.** Validate shape/fields before use (or configure Task Schema Validation on the downstream stage).

## Use-case map

| Use case | Shape |
|---|---|
| SaaS webhook (Stripe/GitHub/Jira/Clicksign…) | connector-event path `conn/event` → verify → `send_task` → `send_json(200)` |
| Generic third-party webhook | `get_raw_request` verify sig → parse → dedupe → `send_task` → `200` |
| Public JSON API endpoint | `get_request` → compute → `send_json(result)` |
| Inbound email ingestion | `get_email_request` → save attachments to `get_persistent_dir()` → `send_task` |
| Payment / form callback | parse → update table → `send_response` (redirect/HTML) |
| Health/status endpoint | `send_json({"status": "ok"})` |

See `hooks_examples.md` for complete, commented hook files.
