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

# Insights and proposals

> Connections Exo suggests between two things a subject knows, why they wait for approval, and how rejecting one teaches the system.

A system that silently invents links in someone's knowledge graph is a system you cannot audit. One that finds candidate links and asks first is one you can. Exo takes the second option: the background cycle proposes edges, and nothing becomes an edge until it is accepted.

Read the queue with [`GET /v1/recall/insights`](/api-reference/recall/get-recall).

```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "type": "insights",
  "items": [
    {
      "proposalId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2_ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81_similar_9f2c4b6a1d8e3f07_5c1a7e9b204d6f38",
      "sourceNodeId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2",
      "sourceTitle": "Pricing working session",
      "targetNodeId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81",
      "targetTitle": "Churn review, March",
      "edgeType": "similar",
      "confidence": 0.44,
      "rationale": "seat, usage, expansion, contract floor"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

## What it is

**Proposal**, a candidate edge the background cycle found but did not write (in practice, a row saying "these two nodes look related, here is how strongly, and here are the terms they share").

**Rationale**, the shared content terms behind the suggestion (in practice, the words the two endpoints actually have in common, so you can judge the suggestion instead of trusting a number). It is null when the overlap is not meaningful enough to name.

**Curation**, your verdict on a proposal (in practice, accept to promote it into a real edge, or reject to discard it and stop it coming back).

The point of the rationale field is that a score alone is not reviewable. `0.44` tells you nothing you can argue with. `"seat, usage, expansion, contract floor"` tells you whether the link is about pricing mechanics or a coincidence of vocabulary.

## Where it comes from

Similarity at ingest is a spectrum, and Exo splits it in two:

* **Clearly related** pairs are linked outright when the content is ingested. They become edges immediately and never appear here.
* **Plausibly related** pairs sit in a narrower band below that. They become proposals and wait for you.

That is why this view is sparse even on a large graph, and why a fresh org with hundreds of nodes can legitimately return an empty `items` array. It is not a failure to find anything. It is the band being deliberately narrow so the queue stays reviewable. The current split links pairs at roughly 0.5 and above and proposes between roughly 0.4 and 0.5; those numbers are tuning, not contract, so read `confidence` rather than assuming a boundary.

Two more filters keep the queue honest:

* **Ownership.** A proposal only surfaces when the effective subject owns both endpoints. It can never expose a teammate's node.
* **Authority.** Both endpoints must clear an authority floor, so low-authority agent narration about a subject does not get proposed as part of their knowledge. See [The knowledge graph](/concepts/knowledge-graph) for how authority is assigned.

## Field reference

| Field                          | Type           | What it means                                                                                                  |
| ------------------------------ | -------------- | -------------------------------------------------------------------------------------------------------------- |
| `proposalId`                   | string         | Stable id. Pass it to accept or reject.                                                                        |
| `sourceNodeId`, `targetNodeId` | string         | The two endpoint nodes.                                                                                        |
| `sourceTitle`, `targetTitle`   | string         | Display labels, resolved from each node's title or content, so you can render the card without two more calls. |
| `edgeType`                     | string or null | The kind of edge being proposed.                                                                               |
| `confidence`                   | number or null | How strongly the proposal is supported, 0 to 1.                                                                |
| `rationale`                    | string or null | The content terms the endpoints share. Null when nothing nameable overlaps.                                    |

### The same object has two shapes

This is worth knowing before you write a client. The recall view returns the display-oriented shape above. The primitive list, [`GET /v1/proposals`](/api-reference/insights/list-proposals), returns the same underlying rows in a leaner shape:

```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "data": [
    {
      "id": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2_ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81_similar_9f2c4b6a1d8e3f07_5c1a7e9b204d6f38",
      "sourceNodeId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2",
      "targetNodeId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81",
      "edgeType": "similar",
      "score": 0.44,
      "rationale": "seat, usage, expansion, contract floor",
      "createdAt": "2026-07-14T02:41:55Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

The id field is `id` rather than `proposalId`, the score field is `score` rather than `confidence`, and the endpoint titles are not resolved. Use the recall view when you are rendering a review UI and the primitive list when you are automating.

## How to use it

Accept promotes the proposal to a real edge. The new edge carries your identity as its author.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl -X POST https://api.get-exo.com/v1/proposals/ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2_ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81_similar_9f2c4b6a1d8e3f07_5c1a7e9b204d6f38/accept \
    -H "X-Exo-API-Key: $EXO_KEY"
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  import os, requests

  r = requests.post(
      "https://api.get-exo.com/v1/proposals/ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2_ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81_similar_9f2c4b6a1d8e3f07_5c1a7e9b204d6f38/accept",
      headers={"X-Exo-API-Key": os.environ["EXO_KEY"]},
  )
  r.raise_for_status()
  print(r.json())
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const r = await fetch(
    "https://api.get-exo.com/v1/proposals/ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2_ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81_similar_9f2c4b6a1d8e3f07_5c1a7e9b204d6f38/accept",
    { method: "POST", headers: { "X-Exo-API-Key": process.env.EXO_KEY! } },
  );
  if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
  console.log(await r.json());
  ```
</CodeGroup>

```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "proposalId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2_ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81_similar_9f2c4b6a1d8e3f07_5c1a7e9b204d6f38",
  "decision": "accepted",
  "edgeId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2_ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81_similar_9f2c4b6a1d8e3f07_5c1a7e9b204d6f38",
  "rejectionRecorded": null
}
```

The created `edgeId` equals the proposal id, so you can follow the object straight through without a lookup.

Rejecting is the more interesting half:

```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "proposalId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2_ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:c04a7e2f5d81_similar_9f2c4b6a1d8e3f07_5c1a7e9b204d6f38",
  "decision": "rejected",
  "edgeId": null,
  "rejectionRecorded": true
}
```

`rejectionRecorded: true` means the pair went into a durable ledger and the background cycle will stop suggesting it. `false` means the proposal left the queue but the pair was not permanently excluded, so it can come back on a later cycle. Read the field rather than assuming; it is the difference between teaching the system and dismissing a card.

Accepting a pair you previously rejected clears the rejection. Acceptance supersedes rejection, in both directions, so a later proposal with the endpoints swapped is not blocked by an old verdict.

<AccordionGroup>
  <Accordion title="Why is my insights view empty?" icon="circle-question">
    Four ordinary reasons, in the order worth checking.

    The background cycle has not run yet on a freshly imported subject. Proposals are written by the cycle, not by ingest.

    There is not enough content for two nodes to land in the proposal band at all.

    Every candidate pair was already linked outright at ingest, which happens when a subject's content is highly repetitive.

    Both endpoints of every candidate did not clear the authority floor, which is common on a subject whose content is mostly agent narration rather than their own writing.

    An empty `items` array with `hasMore: false` is a valid answer to all four.
  </Accordion>

  <Accordion title="What accepting does to retrieval" icon="circle-question">
    Accepting writes a real edge, invalidates the graph cache and emits a `graph.updated` event. It also changes the organization's edge count, which means the ExoBrain is now serving a graph it was not exported against. The next [`POST /v1/retrieve`](/api-reference/retrieve/post-retrieve) reports `degradationReason: "brain_stale"` as a result. That is expected, not a fault. See [Brain path and hybrid path](/concepts/retrieval-paths).
  </Accordion>

  <Accordion title="Why a proposal id returns 404" icon="circle-question">
    A proposal resolves only when the effective subject owns both endpoint nodes and both are still live. If either endpoint was forgotten or belongs to another subject, the proposal drops out of every listing and its id returns 404 [`proposal_not_found`](/errors/proposal_not_found), the same as an id that never existed. Existence cannot be probed.
  </Accordion>

  <Accordion title="Insights versus contradictions" icon="circle-question">
    Both are things the background cycle noticed, and they are opposite in kind. An insight says two things might belong together and asks whether to link them. A [contradiction](/concepts/contradiction) says two things the subject held cannot both be true and does not ask you anything: it is surfaced as a fact about their history, not as a queue item. You resolve a contradiction by [superseding](/concepts/belief-and-supersession) a side, not by accepting or rejecting it.
  </Accordion>
</AccordionGroup>

## Limits

* There is no bulk accept or bulk reject. One decision per call.
* There is no route that regenerates proposals on demand. They appear when the background cycle runs.
* Rejection durability depends on the ledger being present for your organization. When it is not, `rejectionRecorded` comes back false and the pair remains eligible for re-suggestion.
* Proposals carry no explanation beyond `rationale`. There is no route that returns the evidence chain behind a suggestion.

## Next

<Columns cols={2}>
  <Card title="Contradiction" icon="git-compare" href="/concepts/contradiction">
    The other thing the background cycle surfaces, and why it is not a queue.
  </Card>

  <Card title="List edge proposals" icon="code" href="/api-reference/insights/list-proposals">
    The primitive listing, its filters and pagination.
  </Card>
</Columns>
