# Flow Hooks & Custom Tools This document describes how BlueHive HUM integrates external systems (EHRs, CRMs, registration APIs, …) into voice calls **without code changes** — by attaching them to a flow as configurable *tool bindings*. There are two ways a binding runs: | Mode | When it runs | Visible to the model? | Used for | | ------------ | --------------------------------------------- | --------------------- | ------------------------------------------------------------ | | **Pre-call** | Once during connect, before the first prompt | No (rendered into prompt) | Caller lookups, account context, eligibility checks | | **In-call** | When the model calls the function by name | Yes (function tool) | Patient registration, appointment booking, status lookups | Both modes share the same tool registry, the same encrypted secret storage, and the same templating engine. ## Lifecycle ### Pre-call hooks (caller lookup) 1. At connect time, HUM resolves all enabled bindings for the inbound flow. 2. It filters them for pre-call HTTP bindings. 3. The pre-call hooks execute concurrently with a **1500ms total wall-clock budget** and a default per-hook timeout of 1200ms. 4. Each successful hook's response is rendered through its `output_template`; failures render `fallback_template` (or are silently dropped if missing). 5. Rendered blocks are joined and appended to the system prompt as a `# Caller Context` block before the first model turn. 6. Executions are logged asynchronously (best-effort — never blocks the call). ### In-call HTTP tools 1. Any HTTP-runtime, **non-pre-call** binding is exposed to the model as a function tool. 2. When the model calls it, HUM executes the bound HTTP request. 3. The HTTP response is rendered into a string via `output_template` (or `fallback_template` on error) and returned to the model as the function-call output. 4. Each call writes an execution log row. ## HTTP Runtime Config Set on the tool version's runtime config (defaults from the tool author) and overridden by the per-flow binding config. Binding wins on conflict. Headers merge shallowly. ```json { "url": "https://ehr.example.com/api/lookup", "method": "POST", "headers": { "X-Source": "bluehive-hum" }, "auth_type": "bearer", "auth_secret_name": "EHR_API_TOKEN", "body_kind": "json", "body_template": "{\"phone\":\"{{from_e164}}\"}", "query_template": null, "timeout_ms": 1200, "allow_internal": false } ``` | Field | Notes | | ------------------ | ------------------------------------------------------------------------------------------ | | `url` | Required. Must be `http://` or `https://`. Templated. | | `method` | `GET` / `POST` / `PUT` / `PATCH` / `DELETE`. Default `POST`. | | `headers` | Plain object. Values are templated. | | `auth_type` | `none` (default), `bearer`, `basic`, `header`. | | `auth_secret_name` | Looks up an encrypted secret scoped to `(org, tool)`. Decrypted at request time only. | | `auth_header` | When `auth_type=header`, custom header name (e.g. `X-Api-Key`). | | `body_kind` | `json` (default), `form`, or `raw`. | | `body_template` | Templated. For `json`, must render to valid JSON; for `form`, render URL-encoded key/value pairs; for `raw`, the rendered text is sent as-is. | | `query_template` | Templated. Appended to URL as `?...`. | | `timeout_ms` | Default 1200ms (pre-call) / 3000ms (in-call). | | `allow_internal` | Default `false`. SSRF guard rejects private IPs / `localhost` / link-local unless set. | ## Templating The templating engine implements a deliberately small Mustache subset — **no eval, no helpers, no JS expressions, no HTML escaping**. Values are inserted as-is (these templates render into prompt strings and JSON bodies, not HTML). Supported: ``` {{path.to.value}} → ctx.path.to.value, inserted as-is. {{#if cond}}…{{/if}} → truthy block. {{#each items}}…{{/each}} → iterates arrays. Inside: {{this}}, {{@index}}. ``` Pre-call context exposes: - `call_id`, `call_sid`, `org_id`, `flow_id`, `direction` - `from_e164`, `to_e164` - `meta.*` — task context (plus the digits-only phone) In-call context additionally exposes the model's arguments under `args.*` and the response payload under `result.*` / `response.*`. When a template is null, HUM falls back to a pretty-printed JSON dump of the response — useful for quick "just stuff the response in the prompt" hooks. ## Security model - **SSRF**: HUM rejects RFC1918, loopback, link-local, and IPv6 ULA / loopback hosts by default. Set `allow_internal: true` only for trusted internal services on a private network route. - **Protocols**: only `http://` and `https://` are accepted. Other schemes (`file:`, `gopher:`, `data:`) return `error_code: blocked_url`. - **Secrets**: stored encrypted, decrypted only at request time and never logged. - **Response cap**: 256KB. Larger responses are not truncated; reading is aborted and the tool execution fails with `error_code: fetch_failed` (error message: `response exceeded bytes`). - **Audit**: every execution is logged with status (`success | error | timeout | rejected`), latency, and a capped record of arguments / result. ## Example: EHR caller lookup → patient registration **Tool definition:** ``` slug: ehr_caller_lookup runtime_type: http runtime_config: url: https://ehr.example.com/api/v1/patients/lookup method: POST auth_type: bearer auth_secret_name: EHR_API_TOKEN body_template: '{"phone":"{{from_e164}}"}' ``` **Flow binding (pre-call):** ``` pre_call: 1 output_template: | Caller: {{result.first_name}} {{result.last_name}} (DOB {{result.dob}}). Confirm DOB before sharing protected info. fallback_template: | Caller {{from_e164}} not found in EHR. If they confirm they're a new patient, call patient_registration to start intake. ``` **Companion in-call tool binding** for the registration follow-up (`patient_registration` slug, `pre_call: 0`, with its own template). ## Endpoints | Method | Path | Notes | | ------ | --------------------------------------------- | -------------------------------------- | | `POST` | `/v1/flows/:flowId/tools` | Create / upsert binding. | | `PUT` | `/v1/flows/:flowId/tools` | Bulk replace bindings (≤100). | | `PATCH`| `/v1/flows/:flowId/tools/:bindingId` | Partial update; supports new fields. | | `POST` | `/v1/flows/:flowId/tools/:bindingId/test` | Dry-run with arbitrary input. No log. |