> ## 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.

# Phase

> The eras of someone's history, clustered from the dates inside their own content rather than from when it was uploaded.

"What was I working on last spring" is a question a date filter answers badly. You have to guess the range, and the boundary you guess almost never matches where the person's actual attention moved.

Exo clusters dated content into phases so time becomes a structure it can reason over instead of a column to sort by. The question resolves to a chapter, not to a range you had to invent.

Read them from [`GET /v1/recall/temporal`](/api-reference/recall/get-recall).

```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "type": "temporal",
  "phases": [
    { "nodeId": "ph_2a41", "title": "Mar 2026: retrieval quality" },
    { "nodeId": "ph_7c08", "title": "Jun 2026: pricing and packaging" },
    { "nodeId": "ph_9d13", "title": "Sep 2026: enterprise onboarding" }
  ],
  "hasMore": null,
  "nextCursor": null
}
```

## What it is

**Phase**, a chapter of the subject's history that Exo found rather than one you declared (in practice, a cluster of content that sits close together in time and topic, given a label of the form `Mon YYYY: topic`).

A phase is a real node in the graph, with `nodeType: "phase"` and a stable `nodeId`. It is not a view or a computed grouping that disappears between calls. You can drill into it with [`GET /v1/recall/graph/node/{node_id}`](/api-reference/recall/get-recall-graph-node) and see what belongs to it, the same way you would with any other node.

The title is assembled at read time: the month and year come from the cluster, and the topic is resolved from the phase's own member content. So a phase's label can sharpen as more of its content arrives, without its id changing.

## Where it comes from

Phases depend on one thing being right, and it is the reason they work at all: **`createdAt` on a node is the content date, not the ingest date.**

When you import an archive, Exo parses dates out of the content itself, in several forms, and prefers the date the content is *about* over the moment the upload happened. A ten-year ChatGPT export uploaded on a Tuesday keeps ten years of real dates.

If it stamped ingest time instead, every imported history would collapse into a single phase on the day of the upload, and this entire object would be useless. That is worth knowing because it explains the failure you will see if a source arrives without usable dates: undated content cannot be clustered into an era.

Clustering itself runs in the background cognition cycle, not at ingest. Content is retrievable long before it is phased.

```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
flowchart LR
  A[Content with<br/>real dates] --> B[Nodes stamped with<br/>the content date]
  B --> C[Background<br/>cognition cycle]
  C --> D[Phase nodes]:::focus
  D --> E[GET /v1/recall/temporal]
  classDef focus stroke-width:2px;
```

## Field reference

| Field    | Type   | What it means                                                                                                 |
| -------- | ------ | ------------------------------------------------------------------------------------------------------------- |
| `nodeId` | string | Stable id of the phase. It is a graph node id, usable anywhere one is accepted.                               |
| `title`  | string | Human-readable label in the form `Mon YYYY: topic`, with the topic resolved at read time from member content. |

The object is deliberately small. A phase is a handle onto a region of the graph, and everything interesting about it is reachable through that handle rather than duplicated onto it.

## How to use it

List phases, then walk into one.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl "https://api.get-exo.com/v1/recall/temporal" \
    -H "X-Exo-API-Key: $EXO_KEY"

  curl "https://api.get-exo.com/v1/recall/graph/node/ph_7c08" \
    -H "X-Exo-API-Key: $EXO_KEY"
  ```

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

  h = {"X-Exo-API-Key": os.environ["EXO_KEY"]}

  phases = requests.get(
      "https://api.get-exo.com/v1/recall/temporal", headers=h
  ).json()["phases"]

  for phase in phases:
      print(phase["nodeId"], phase["title"])

  detail = requests.get(
      f"https://api.get-exo.com/v1/recall/graph/node/{phases[0]['nodeId']}",
      headers=h,
  ).json()
  print(detail["label"], len(detail["edges"]))
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const h = { "X-Exo-API-Key": process.env.EXO_KEY! };

  const temporal = await fetch(
    "https://api.get-exo.com/v1/recall/temporal",
    { headers: h },
  ).then((r) => r.json());

  for (const phase of temporal.phases) {
    console.log(phase.nodeId, phase.title);
  }

  const detail = await fetch(
    `https://api.get-exo.com/v1/recall/graph/node/${temporal.phases[0].nodeId}`,
    { headers: h },
  ).then((r) => r.json());
  console.log(detail.label, detail.edges.length);
  ```
</CodeGroup>

The temporal view paginates only when you ask it to. Send `cursor` or `pageSize` and the response gains `hasMore` and `nextCursor`; omit both and you get the whole list with those fields null. That is why they are null in the payload at the top of this page.

## What phases enable

The reason to build eras rather than filter by date:

* **Chapter-shaped questions.** "What changed between the pricing work and the enterprise push" is answerable against two phase nodes and the edges between them.
* **Stable handles for a moving boundary.** A date range you hardcode goes stale as content arrives. A phase id keeps pointing at the same era while the era's own extent adjusts.
* **Ordering for belief change.** Phases give a coarse timeline to hang [supersession chains](/concepts/belief-and-supersession) on, so "when did they change their mind" has an era to name and not only a timestamp.

<AccordionGroup>
  <Accordion title="Why is my phases array empty?" icon="circle-question">
    Three ordinary causes, in the order worth checking.

    The background cognition cycle has not run since the content landed. Phases are written by the cycle, never by ingest.

    There is not enough dated history to cluster. A subject with a few days of content has one era, and one era is not a useful partition, so nothing is emitted.

    The content arrived without usable dates. Content that carries no parseable date cannot be placed in time, so it cannot join a phase.

    An empty array is a valid answer to all three. It is not an error.
  </Accordion>

  <Accordion title="Why does a phase title change between reads?" icon="circle-question">
    The month and year are fixed by the cluster. The topic half is resolved at read time from the phase's most central member content, so as more of that era arrives, the topic can sharpen. The `nodeId` does not change, which is why you should key on the id and render the title.
  </Accordion>

  <Accordion title="Can I create a phase myself?" icon="circle-question">
    No. There is no route that creates or edits a phase, and phases are not something you assert. If you want your own grouping over content, assert edges between the nodes you care about with [`POST /v1/graph/edges`](/api-reference/graph/assert-edge), which gives you a structure you control alongside the discovered one.
  </Accordion>

  <Accordion title="How phases relate to temporal edges" icon="circle-question">
    Two different mechanisms over the same time information. Phase nodes are the coarse partition: which era does this belong to. Temporal edges are the fine ordering: `temporal_preceded` links content in sequence inside a domain, and `temporal_cross` links content that is close in time across domains. You read phases from the temporal view and edges from [`GET /v1/graph/edges`](/api-reference/graph/list-graph-edges) with `?type=`.
  </Accordion>
</AccordionGroup>

## Limits

* Phases exist only after the background cognition cycle has clustered enough dated content. A freshly imported subject has none.
* The object carries `nodeId` and `title` and nothing else. There is no start date, end date or member count field on it. Drill into the node for its neighbourhood.
* There is no route to create, rename or delete a phase.
* Content without a parseable date does not enter a phase.
* Titles are generated. Treat them as labels for humans, not as stable keys.

## Next

<Columns cols={2}>
  <Card title="Belief and supersession" icon="history" href="/concepts/belief-and-supersession">
    What changed inside an era, and where the authority went.
  </Card>

  <Card title="Recall cognitive state" icon="code" href="/api-reference/recall/get-recall">
    The endpoint behind all four views, including `temporal`.
  </Card>
</Columns>
