### Why / What / How
**Why:** Our Exa integration had fallen behind Exa's API, and part of it
was broken.
- All four Exa Research blocks fail on every run. Exa retired
`/research/v1` on 2026-04-01, and it now answers `410 RESEARCH_RETIRED`.
- Exa's newer products (the Agent API and Search Monitors) had no blocks
at all.
- The Search, Contents and Answer blocks offered search types and
categories Exa has since replaced, and lacked its current options.
- The Websets webhook trigger has never worked. It registered webhooks
at a URL that returns 404, sent Exa the wrong event names, and read the
wrong payload fields.
Linear: OPEN-3789.
**What:** Every current Exa endpoint relevant to workflows now has a
block. Existing blocks expose Exa's current options. Saved workflows
keep working: no block ID, input name, output name or enum value was
removed or renamed.
**How:** The new endpoints go through one small HTTP client
(`_exa_rest.py`) instead of `exa-py`. The installed SDK rejects values
Exa now returns (`pending`, `canceled`, `failed`), drops response fields
we need (`output`, `warnings`, `entities`), and checks options against
an allow-list that lags behind the API. Errors carry Exa's own message
and tag.
### Changes 🏗️
**Broken blocks fixed**
- The four Research blocks run again, on Exa Agent (`/agent/runs`), with
the same IDs, inputs and outputs.
- Model maps to effort: fast→low, standard→medium, pro→high.
- Statuses map back to the old vocabulary.
- `cost_pages` and `cost_reasoning_tokens` are still emitted, as 0,
because Exa Agent doesn't report them.
- The Websets trigger now works and is enabled:
- correct URL (`/websets/v0/webhooks`)
- Exa's own event names
- Exa's payload fields
- the `webset_id` filter is actually applied
- five new `monitor.*` events, off by default
**New blocks (30)**
- **Exa Agent (8):** run (waits by default), get, wait, list, cancel,
stop, delete, list events.
- Covers all seven efforts with their prices, output schemas with
field-level citations, input and exclusion records, follow-up runs, Exa
Connect data sources, and cost and duration budgets.
- **Search Monitors (10):** create, get, list, update, delete, batch,
trigger now, list runs, get run.
- Plus a **Search Monitor Trigger** that starts a workflow on each
delivery and checks Exa's signature once the monitor's secret is pasted
in.
- **Websets (12):**
- webhook create/get/update/delete/list
- webhook delivery attempts
- event list/get
- Websets monitor runs list/get
- import update
- team concurrency info
**Existing blocks brought up to date**
- **Search:**
- Types: `auto`, `lite`, `deep-lite`, `deep`. The legacy `fast`,
`instant`, `deep-reasoning`, `neural` and `keyword` are kept, labelled
with their replacements.
- Categories: `people` and `publication` added. `research paper` is sent
as `publication` and `linkedin profile` as `people`, per Exa's
changelog.
- New options: structured output (`output_schema` → `output_content` /
`output_grounding`), `system_prompt`, `additional_queries`, `objective`,
`user_identifier`.
- Outputs Exa's `warnings` and `notices`.
- Checks lite's parameter allow-list before calling, instead of
surfacing a bare 400.
- Crawl-date filters are no longer sent; Exa ignores them.
- **Contents settings** (Search, Find Similar, Contents):
- New: `max_age_hours`, `snapshot_as_of`, text verbosity and section
filters, highlights `max_characters`, `rich_image_links`.
- Legacy `livecrawl` and `num_sentences` are translated with Exa's
documented mappings. Deprecated params are no longer sent by default.
- **Answer:** `model` (exa / exa-pro / exa-fast / exa-research),
structured answers (`answer_structured`), `system_prompt`,
`user_location`, `user_identifier`, and cost output and billing.
- **Find Similar:** marked deprecated by Exa (pointing to Search), with
`exclude_source_domain` and `category` added.
- **Websets audit**, for example:
- the preview request body was wrong;
- canceled searches waited until timeout because the code checked
`"cancelled"`;
- imports were sent under the wrong key;
- added title, recall, maxPeoplePerCompany, exclude sources, the `url`
enrichment format, and the list/item filters.
- **Labels:** Search Type and Category now show readable labels instead
of `ExaSearchTypes` and a lowercase `category`, as do the new blocks'
enum and object fields.
**Billing and safety**
- **Blocks that keep or spend something in the Exa account need the
user's own Exa key.** Everyone on the platform's Exa key shares one Exa
account, so Websets, their searches, items, enrichments, monitors and
imports, Exa Agent and Research runs, Search Monitors, and the Websets
webhook and event blocks refuse the platform's shared key ("Use Credits
for Exa search" and `exa-default`) with a message telling the user to
add their own key. Batch Search Monitors could otherwise delete every
user's monitors, and a Websets webhook would forward everyone's events.
Search, Answer, Contents, Find Similar, Code Context and a plain Preview
Webset still run on the platform key and are billed from Exa's reported
cost.
- **Exa Agent and Research runs therefore only happen on the user's own
key.** Exa bills those runs to the user's Exa account and the platform
charges no credits for them (the Exa base cost only applies to the
platform's keys). The blocks still report the run's Exa cost once per
user per run: several blocks can see the same finished run (Exa Agent,
Get, Wait, Cancel, Stop and the Research blocks), and a Redis `SET NX`
claim keeps that reported figure from being counted twice. If Redis
can't be reached within 5s, the figure is recorded anyway.
- **Preview Webset with "include sample items"** runs a real Websets
search, and Exa's preview response carries no cost to bill, so that
option needs the user's own key. A plain preview stays allowed on the
platform key.
- **Up-front refusal.** Blocks that refuse the platform key declare it
(`refuses_platform_credentials`), so graph validation rejects a platform
credential on them before the graph runs or is scheduled, with the same
message used for refused SDK defaults, and the builder no longer
auto-selects the platform credential for them. The check inside `run()`
stays as the backstop.
- **Long waits fit the executor cap.** Exa Agent / Wait for Exa Agent
Run (up to 3 h), Create Research / Wait For Research (1 h) and the
Websets wait blocks (30 min) set `execution_timeout_seconds` to their
input's ceiling plus 5 minutes, so their own timeout output fires
instead of the executor cancelling `run()`. Run-starting blocks yield
the run ID before waiting.
- Exa REST calls send writes and deletes once (no automatic retry of
POST/PATCH/DELETE), retry reads a bounded number of times, and drop the
`x-api-key` header on a cross-origin redirect.
- IDs are URL-escaped in every path.
- The Agent API's nested `{"error": {...}}` responses are shown as
readable messages.
**Follow-up fixes in this PR (A/B/C)**
- **A. Preview Webset sample items** ran a real search on the shared key
with nothing billed. That option now refuses the platform key; a plain
preview doesn't.
- **B. AutoPilot couldn't run any Exa block for users without their own
key** (pre-existing on `dev`). The credential matcher counted the SDK
`exa-default` credential as the user's own, so a watched chat picked it
and the credentials manager then refused it, and a user with their own
key was asked to choose between it and `exa-default`. An SDK default
that isn't runnable for its provider (not in
`RUNNABLE_SDK_DEFAULT_PROVIDERS`) now never matches a credential field,
so the "Use Credits" system credential is used. Runnable SDK defaults
(e.g. Tavily) are unchanged.
- **C. Nothing told the system which blocks refuse the shared key**, so
graphs with them saved, scheduled and ran and only failed at the node.
The new `refuses_platform_credentials` block marker is read by graph
validation and exposed on the block's credentials field schema so the
builder doesn't auto-select a system credential for such a block. Exa's
stateful blocks and SmartLead's campaign blocks set it; a test keeps the
marker and the run-time refusal in agreement.
- Removed the Exa-only `_platform_key` guard: every block using it
already refused the shared key in `run()` first, so it never fired.
**Docs:** `docs/integrations/block-integrations/exa/` was regenerated,
with hand-written "how it works" sections for every new block and
updated sections for the changed ones.
**Behaviour changes worth knowing**
- Search now forwards all of its content settings. Before, it silently
dropped livecrawl, subpages, extras and context.
- Invalid inputs now fail early with a clear message instead of an Exa
400. Examples: a non-ISO country code, lite with unsupported options,
more than 5 webset criteria, `refresh` Websets monitors (removed by
Exa).
- `Webset.status` is a plain string now, so new Exa statuses don't crash
runs.
- Saved Highlights settings with only a query now get Exa's adaptive
length instead of 1 sentence. Saved graphs that stored the full default
object behave the same.
**Found and left out of scope**
- Exa's webhook signatures carry a timestamp that is not checked for
replay. Exa documents the check as optional and doesn't say whether
retried deliveries are re-signed, so a fixed window could drop
legitimate retries. This predates the PR; the new trigger uses the same
check.
- Create Research is still declared a read (`effect=READ`) although it
starts a paid run, as it was on `dev`; Exa Agent now declares
`EXTERNAL_JUDGED`.
- Most Websets item/enrichment/import-create/export calls still use
`exa-py`, whose models match the spec there.
### Screenshots (local stack: `dev` at 5bea25d vs this branch, same
browser and account)
**Block menu, searching "Exa"**: new blocks appear (Exa Agent Run, Get
Agent Run, …), and the Research blocks say they run on Exa Agent.
| Before | After |
|---|---|
| 
| 
|
**Exa Search, Search Type**: Exa's current types, with legacy values
kept for saved workflows. The label was "ExaSearchTypes".
| Before | After |
|---|---|
| 
| 
|
**Exa Search, Category**
| Before | After |
|---|---|
| 
| 
|
**Exa Answer**: model, structured output, system prompt, location.
| Before | After |
|---|---|
| 
| 
|
**Exa Create Research**: same inputs, now backed by Exa Agent.
| Before | After |
|---|---|
| 
| 
|
**New blocks: Exa Agent Run and Exa Create Search Monitor**
| Exa Agent Run | Exa Create Search Monitor |
|---|---|
| 
| 
|
Full Exa Search node:
[before](https://github.com/user-attachments/assets/52288ed2-72ac-4b04-9fba-64dbda77bef6)
·
[after](https://github.com/user-attachments/assets/3f612de4-9db0-4465-9932-28e5312cc5db)
### Agents and large language models used
- Claude Code with Claude Opus 5.5
### Checklist 📋
#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [ ] I have tested my changes according to the test plan:
- [x] `pytest backend/blocks/exa
backend/blocks/block_cost_tracking_test.py`: 345 passed
- [x] `pytest backend/blocks/test/test_block.py -k Exa`: 474 passed
(every Exa block's test input, output and mock)
- [x] `pytest backend/util/architecture_test.py`: 16 passed
- [x] black, isort, ruff and pyright clean on changed files;
`generate_block_docs.py --check` in sync
- [x] Saved-graph compatibility: imported every pre-existing Exa block
from `dev` and this branch and compared the input/output schemas
recursively. No ID, field or enum value was removed.
- [x] Builder renders every changed and new block (screenshots above)
- [ ] Live calls against Exa with a real key. No key was available while
building, so payloads were checked against Exa's published OpenAPI spec
(`exa.ai/docs/exa-spec.yaml`) and mocked responses. **Worth one run each
of Exa Agent, Search (deep + output schema), Answer (model), Create
Search Monitor and the Webset trigger on a preview with a real key
before merging.**
#### For configuration changes:
- [x] `.env.default` is updated or already compatible with my changes
- [x] `docker-compose.yml` is updated or already compatible with my
changes
- [x] I have included a list of my configuration changes in the PR
description (under **Changes**). None.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
2.6 KiB
Exa Code Context
Blocks for searching code repositories and documentation using Exa's code context API.
Exa Code Context
What it is
Legacy: Exa no longer documents its code context API; for new workflows use the Exa Search block, which Exa's Code & Docs guide recommends (e.g. type 'lite' with highlights). Searches GitHub repos, docs and Stack Overflow for relevant code examples.
How it works
This block sends your query to Exa's /context endpoint, which Exa no longer documents: it is missing from Exa's API spec and its reference page redirects to the Code & Docs search guide. The block is kept so saved workflows keep running; for new workflows, use the Exa Search block (for example type 'lite' with highlights), as that guide recommends.
The endpoint returns one formatted string of code snippets and examples with their sources from GitHub repositories, documentation and Stack Overflow, plus the result count, search time, output token count and cost. Tokens Num sets the response size: 'dynamic' (default) or a token count such as 5000 or 10000. The run is billed from the dollar cost Exa reports, at 150 credits per US dollar.
Inputs
| Input | Description | Type | Required |
|---|---|---|---|
| query | Search query to find relevant code snippets. Describe what you're trying to do or what code you're looking for. | str | Yes |
| tokens_num | Token limit for response. Use 'dynamic' for automatic sizing, 5000 for standard queries, or 10000 for comprehensive examples. (In Exa Search the equivalent is text max characters.) | str | int | No |
Outputs
| Output | Description | Type |
|---|---|---|
| error | Error message if the operation failed | str |
| request_id | Unique identifier for this request | str |
| query | The search query used | str |
| response | Formatted code snippets and contextual examples with sources | str |
| results_count | Number of code sources found and included | int |
| cost_dollars | Cost of this request in dollars | str |
| search_time | Time taken to search in milliseconds | float |
| output_tokens | Number of tokens in the response | int |
Possible use case
API Integration Examples: Find real-world code examples showing how to integrate with specific APIs or libraries.
Debugging Assistance: Search for code patterns related to error messages or specific programming challenges.
Learning New Technologies: Discover implementation examples when learning a new framework or programming language.