# Saving structured data (https://brainapi.lumen-labs.ai/docs/v2/ingestion/structured-data)

> For the complete BrainAPI documentation index, see [llms.txt](https://brainapi.lumen-labs.ai/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL (e.g. https://brainapi.lumen-labs.ai/docs/v2/ingestion/structured-data.md).

Ingest direct or event-centric triple sets into the knowledge graph

Structured ingestion lets you push **already-shaped facts** into the brain instead of asking an LLM to infer them from free text. Use direct triples for timeless structure such as product attributes and preferences; use event-centric triples for actions whose identity or time matters.

`POST /ingest/structured` accepts a list of `IngestionTripleSet` objects, an optional **anchor** that ties the new facts into an existing node, and optional **text** for extra context. The API returns **202 Accepted** with a `task_id`. Poll status with [task polling](https://brainapi.lumen-labs.ai/docs/v2/ingestion/tasks).

<AgentNote>
- `POST https://brainapi.lumen-labs.ai/ingest/structured` (or your deploy URL) with `BrainPAT` + brain scope (`X-Brain-ID` / `brain_id`)
- Body: `{ "data": [IngestionTripleSet, …], "anchor"?, "text"?, "mode"?, "brain_id"? }` — **not** old `json_data` / element schema
- Expect **202** + `task_id`; poll `GET /tasks/{id}`; **404** = unknown task
- Direct triple: required `subject` + `subj_event` + `object`, with `event` and `event_obj` omitted
- Event-centric triple: `event` and `event_obj` must both be present; `object` is always required
</AgentNote>

<Callout type="warn">
  The previous element-based payload (`json_data`, `metadata`, `types`,
  `identification_params`, `textual_data`, `observate_for`) is removed. Send
  event-centric triples only.
</Callout>

## Choose direct or event-centric shape

Direct edges represent stable structure without manufacturing an event:

```text
[subject] --subj_event--> [object]

PRODUCT --HAS--> ATTR
USER --PREFERS--> ATTR
```

Event-centric facts preserve an action, timestamp, or provenance-bearing hub:

```text
[subject?] --subj_event--> [event] --event_obj--> [object]
                              ^
                         optional anchor
```

`event` and `event_obj` are a pair: sending only one is a validation error. A legacy `HAS → HAS-event → HAS` wrapper with no `happened_at` is collapsed to one direct `HAS` edge. A dated event is preserved as two edges.

## Processing modes

| Mode | Submitted triples | Optional `text` | LLM behavior |
| --- | --- | --- | --- |
| `deterministic` | Persisted directly | Ignored for enrichment | No Scout/Architect; anchor must resolve exactly |
| `hybrid` | Persisted first | May add context/enrichment | Optional LLM enrichment without re-persisting submitted triples |
| `enrich` | Persisted first | Used for backfill | Explicit LLM enrichment path |

When `mode` is omitted, BrainAPI chooses `hybrid` if `text` is present and `deterministic` otherwise. Deterministic anchors resolve by UUID or exact `name` + `type` and fail closed when unresolved.

### Direct preference example

```json
{
  "data": [
    {
      "subject": {"uuid": "user:u01", "name": "u01", "type": "USER"},
      "subj_event": {"name": "PREFERS", "amount": 0.8},
      "object": {"uuid": "hub:attr:70s", "name": "70s", "type": "ATTR"}
    }
  ],
  "mode": "deterministic",
  "brain_id": "commerce"
}
```

## Quick path

Submit one purchase event anchored to a user, then poll the task:

```bash
curl -X POST "<DEPLOYMENT_URL>/ingest/structured" \
  -H "Content-Type: application/json" \
  -H "BrainPAT: YOUR_BRAIN_PAT" \
  -H "X-Brain-ID: ecommerce" \
  -d '{
    "data": [
      {
        "subject": {
          "name": "Alice",
          "type": "Person",
          "properties": { "user_id": "123" }
        },
        "subj_event": { "name": "MADE" },
        "event": {
          "name": "Product purchase",
          "type": "Event",
          "happened_at": "2026-01-01T00:00:00Z"
        },
        "event_obj": { "name": "TARGETED" },
        "object": {
          "name": "Product 1",
          "type": "Product",
          "description": "Wireless headphones",
          "properties": { "product_id": "sku-123" }
        }
      }
    ],
    "anchor": { "name": "Alice", "type": "Person" },
    "text": "Alice purchased Product 1 during checkout.",
    "brain_id": "ecommerce"
  }'
```

Expected response shape:

```json
{
  "message": "Structured ingestion accepted",
  "task_id": "…"
}
```

HTTP status: **202**.

## Example payloads

<Tabs
  items={[
    "Example 1: Social Network",
    "Example 2: Ecommerce",
    "Example 3: News App",
  ]}
>
  <Tab value="Example 1: Social Network">
    A user likes a post. Anchor the batch to the liking user so the event hangs off their graph.
    <DynamicCodeBlock
      lang="json"
      code={`{
  "data": [
    {
      "subject": {
        "name": "John Doe",
        "type": "Person",
        "properties": { "user_id": "123" }
      },
      "subj_event": { "name": "MADE" },
      "event": {
        "name": "Post liked",
        "type": "Event",
        "happened_at": "2026-01-01T00:00:00Z",
        "properties": { "action": "POST_LIKED" }
      },
      "event_obj": { "name": "TARGETED" },
      "object": {
        "name": "Post 123",
        "type": "Post",
        "description": "This is the title of the post that was liked.",
        "properties": { "post_id": "123", "author_id": "312" }
      }
    }
  ],
  "anchor": { "name": "John Doe", "type": "Person" },
  "text": "John Doe liked post 123.",
  "brain_id": "socialnetwork"
}`}
    />
  </Tab>
  <Tab value="Example 2: Ecommerce">
    A purchase event targeting a product, anchored to the buyer.
    <DynamicCodeBlock
      lang="json"
      code={`{
  "data": [
    {
      "subject": {
        "name": "Alice",
        "type": "Person",
        "properties": { "user_id": "123" }
      },
      "subj_event": { "name": "MADE" },
      "event": {
        "name": "Product purchased",
        "type": "Event",
        "happened_at": "2026-01-01T00:00:00Z"
      },
      "event_obj": { "name": "TARGETED" },
      "object": {
        "name": "Product 1",
        "type": "Product",
        "description": "This will be the description of the product that was purchased.",
        "properties": {
          "product_id": "123",
          "other_product_ids_in_cart": ["123", "456", "789"]
        }
      }
    }
  ],
  "anchor": { "name": "Alice", "type": "Person" },
  "text": "Alice purchased Product 1.",
  "brain_id": "ecommerce"
}`}
    />
  </Tab>
  <Tab value="Example 3: News App">
    A read event with optional amount on the predicate (read percentage).
    <DynamicCodeBlock
      lang="json"
      code={`{
  "data": [
    {
      "subject": {
        "name": "John Doe",
        "type": "Person",
        "properties": { "user_id": "123" }
      },
      "subj_event": { "name": "MADE" },
      "event": {
        "name": "Article read",
        "type": "Event",
        "happened_at": "2026-01-01T00:00:00Z",
        "properties": {
          "action": "READ_ARTICLE",
          "article_topics": ["Technology", "Science", "Health"]
        }
      },
      "event_obj": {
        "name": "TARGETED",
        "amount": 68
      },
      "object": {
        "name": "Article 1",
        "type": "Article",
        "description": "This will be the content of the article that was read.",
        "properties": {
          "article_id": "123",
          "article_tags": ["Technology", "Science", "Health"],
          "article_polarity": "positive",
          "article_author": "John Doe"
        }
      }
    }
  ],
  "anchor": { "name": "John Doe", "type": "Person" },
  "text": "John Doe read 68% of Article 1.",
  "brain_id": "newsapp"
}`}
    />
  </Tab>
</Tabs>

## Usage

Available via REST, Python SDK, and TypeScript SDK.

<Tabs items={["Typescript", "Python", "cURL"]}>
  <Tab value="Typescript">
    <DynamicCodeBlock
      lang="ts"
      code={`const result = await brain.ingestStructuredData({
  data: [
    {
      subject: { name: "Alice", type: "Person" },
      subj_event: { name: "MADE" },
      event: { name: "Product purchase", type: "Event", happened_at: "2026-01-01T00:00:00Z" },
      event_obj: { name: "TARGETED" },
      object: { name: "Product 1", type: "Product" },
    },
  ],
  anchor: { name: "Alice", type: "Person" },
  text: "Alice purchased Product 1.",
  brain_id: "example01",
});
// result.task_id — poll GET /tasks/{task_id}`}
    />
  </Tab>
  <Tab value="Python">
    <DynamicCodeBlock
      lang="py"
      code={`result = await client.ingest_structured_data(
    data=[
        {
            "subject": {"name": "Alice", "type": "Person"},
            "subj_event": {"name": "MADE"},
            "event": {
                "name": "Product purchase",
                "type": "Event",
                "happened_at": "2026-01-01T00:00:00Z",
            },
            "event_obj": {"name": "TARGETED"},
            "object": {"name": "Product 1", "type": "Product"},
        }
    ],
    anchor={"name": "Alice", "type": "Person"},
    text="Alice purchased Product 1.",
    brain_id="example01",
)
# result["task_id"] — poll GET /tasks/{task_id}`}
    />
  </Tab>
  <Tab value="cURL">
    <DynamicCodeBlock
      lang="bash"
      code={`curl -X POST "<DEPLOYMENT_URL>/ingest/structured" \\
  -H "Content-Type: application/json" \\
  -H "BrainPAT: YOUR_BRAIN_PAT" \\
  -d '{
    "data": [{
      "subject": {"name": "Alice", "type": "Person"},
      "subj_event": {"name": "MADE"},
      "event": {"name": "Product purchase", "type": "Event"},
      "event_obj": {"name": "TARGETED"},
      "object": {"name": "Product 1", "type": "Product"}
    }],
    "anchor": {"name": "Alice", "type": "Person"},
    "text": "Alice purchased Product 1.",
    "brain_id": "example01"
  }'`}
    />
  </Tab>
</Tabs>

## Request body

<TypeTable
  type={{
    data: {
      description: "List of event-centric information triples to ingest.",
      type: "IngestionTripleSet[]",
      required: true,
    },
    anchor: {
      description:
        "Optional node to connect the structured batch to. Must be an object with uuid, or both name and type — not a string.",
      type: "PartialNodeFilter",
      required: false,
    },
    text: {
      description: "Additional free-text context for the structured data.",
      type: "string",
      required: false,
    },
    mode: {
      description: "Processing mode: deterministic, hybrid, or enrich. Defaults to hybrid when text is present and deterministic otherwise.",
      type: "string",
      required: false,
    },
    brain_id: {
      description: "Brain identifier to store the data in.",
      type: "string",
      required: false,
      default: "default",
    },
  }}
/>

### `IngestionTripleSet`

| Field | Required | Description |
| --- | --- | --- |
| `subject` | conditional | Required with `subj_event` for a direct triple; optional actor for an event-centric triple. |
| `subj_event` | conditional | Required with `subject` for a direct triple; optional subject → event predicate for an event-centric triple. |
| `event` | paired | Event hub node. Must be supplied together with `event_obj`, or both omitted. |
| `event_obj` | paired | Event → object predicate. Must be supplied together with `event`, or both omitted. |
| `object` | **yes** | Target node (product, post, article, …). |

### Node fields (`RequestPartialNode`)

<TypeTable
  type={{
    name: {
      description: "Display name of the node.",
      type: "string",
      required: true,
    },
    type: {
      description: "Node type (e.g. Person, Event, Product). Labels default to [type] when omitted.",
      type: "string",
      required: true,
    },
    uuid: {
      description: "Existing node id when you already know it.",
      type: "string",
      required: false,
    },
    labels: {
      description: "Optional labels; defaults to [type].",
      type: "string[]",
      required: false,
    },
    description: {
      description: "Short description of the node.",
      type: "string",
      required: false,
    },
    properties: {
      description: "Arbitrary key/value properties (ids, tags, metadata).",
      type: "object",
      required: false,
      default: "{}",
    },
    happened_at: {
      description: "ISO datetime when the node happened (mostly for events).",
      type: "string",
      required: false,
    },
  }}
/>

### Predicate fields (`RequestPartialPredicate`)

<TypeTable
  type={{
    name: {
      description: "Relationship / predicate name.",
      type: "string",
      required: true,
    },
    uuid: {
      description: "Existing relationship id when known.",
      type: "string",
      required: false,
    },
    description: {
      description: "Optional description of the relationship.",
      type: "string",
      required: false,
    },
    amount: {
      description: "Numeric amount when the link is quantitative (e.g. read %).",
      type: "number",
      required: false,
    },
  }}
/>

### `anchor` (`PartialNodeFilter`)

Must include either `uuid`, **or** both `name` and `type`. Plain strings are rejected.

<TypeTable
  type={{
    uuid: {
      description: "Resolve the anchor by node uuid.",
      type: "string",
      required: false,
    },
    name: {
      description: "Resolve by name (requires type).",
      type: "string",
      required: false,
    },
    type: {
      description: "Resolve by type (requires name).",
      type: "string",
      required: false,
    },
    meta_description: {
      description: "Extra hint for entity resolution.",
      type: "string",
      required: false,
    },
  }}
/>

## Related

- [How to poll ingestion tasks](https://brainapi.lumen-labs.ai/docs/v2/ingestion/tasks)
- [Retrieve context](https://brainapi.lumen-labs.ai/docs/v2/retrieval/context)
- [Catalog search and personalization](https://brainapi.lumen-labs.ai/docs/v2/retrieval/search/catalog-personalization)
- [Saving text](https://brainapi.lumen-labs.ai/docs/v2/ingestion/text)
