---
title: Declaring *target state*
description: >
  Target states describe what should exist in an external system — files, rows,
  embeddings. Learn how to declare them through connector APIs, nest container
  targets, and rely on CocoIndex to sync minimal creates, updates, and deletes.
---
:::note[Prerequisite]
This page builds on [Core Concepts](./core_concepts), which introduces target states and the declarative *target state = transform(source state)* model **with diagrams**. If the ideas below feel abstract, start there.
:::

A **target state** represents what you want to exist in an external system. You *declare* target states in your code; CocoIndex keeps them in sync with your intent — creating, updating, or removing them as needed.

:::note[Terminology]
A **target** is the external system you write to — a directory, a database table, a vector store collection, etc. In Python, targets are represented by objects like `DirTarget` and `TableTarget`. A **target state** is what you want to exist *in* that target — a specific file, row, or embedding.

CocoIndex treats your declarations as the source of truth: if you stop declaring a target state, CocoIndex will remove it from the target.
:::

Examples of target states:

- A file in a directory
- A row in a database table
- An embedding vector in a vector store

When your source data changes, CocoIndex compares the newly declared target states with those from the previous run and applies only the necessary changes.

## Declaring target states

CocoIndex connectors provide **targets** with `declare_*` methods:

```python
# Declare a file target state
dir_target.declare_file(filename="output.html", content=html)

# Declare a row target state
table_target.declare_row(row=DocEmbedding(...))
```

### Where do targets come from?

Target states can be nested — a directory contains files, a table contains rows. The container itself is a target state you declare, and once it's ready, you get a target to declare child target states within it.

Container target states (like a directory or table) are typically top-level — you can declare them directly. Child target states (like files or rows) require the container to be ready first.

Connectors provide convenience methods that mount the container and return a ready-to-use target in one step:

### Example: writing a file to a directory

For simple cases where each processing component writes a single file, you can declare the file directly:

```python
from cocoindex.connectors import localfs

# Declare a single file target state directly
localfs.declare_file(outdir / "output.html", html, create_parent_dirs=True)
```

When you need a `DirTarget` to declare multiple files, use the connector's convenience method:

```python
# Mount a directory target, get a ready-to-use DirTarget
dir_target = await localfs.mount_dir_target(outdir)

# Declare child target states (files)
dir_target.declare_file(filename="output.html", content=html)
```

### Example: writing a row to PostgreSQL

This example uses a [`ContextKey`](./context) to reference the database connection — see [Context](./context) for how keys are defined and provided.

```python
import asyncpg
import cocoindex as coco
from cocoindex.connectors import postgres

# Define a ContextKey for the database connection (provided in lifespan)
TARGET_DB = coco.ContextKey[asyncpg.Pool]("target_db")

# Mount a table target, get a ready-to-use TableTarget
table = await postgres.mount_table_target(
    TARGET_DB,
    table_name="doc_embeddings",
    table_schema=await postgres.TableSchema.from_class(
        DocEmbedding, primary_key=["id"]
    ),
)

# Declare a child target state (a row)
table.declare_row(row=DocEmbedding(...))
```

These convenience methods wrap [`mount_target()`](./processing_component#mount_target), which automatically derives the component path from the target's globally unique key. See [Processing Component](./processing_component) for more on mounting APIs.

:::tip[Type safety]
Targets like `DirTarget` and `TableTarget` have two statuses: **pending** (just created) and **resolved** (after the container target state is ready). The type system tracks this — if you try to use a pending target before it's resolved, type checkers like mypy will flag the error.
:::

## How CocoIndex syncs target states

Under the hood, CocoIndex compares your declared target states with the previous run and applies the minimal changes needed — the same create/update/delete sync the [change-scenario diagrams in Core Concepts](./core_concepts#processing-component) walk through, viewed per target state:

<table>
  <thead>
    <tr>
      <th rowspan="2">Target State</th>
      <th colspan="3" style={{textAlign: 'center'}}>CocoIndex's Action</th>
    </tr>
    <tr>
      <th>On first declaration</th>
      <th>When declared differently</th>
      <th>When no longer declared</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>A database table</td>
      <td>Create the table</td>
      <td>Alter the table</td>
      <td>Drop the table</td>
    </tr>
    <tr>
      <td>A row in a database table</td>
      <td>Insert the row</td>
      <td>Update the row</td>
      <td>Delete the row</td>
    </tr>
    <tr>
      <td>A file in a directory</td>
      <td>Create the file</td>
      <td>Update the file</td>
      <td>Delete the file</td>
    </tr>
  </tbody>
</table>

CocoIndex ensures containers exist before their contents are added, and properly cleans up contents when the container changes.

### What happens when a target's schema changes

When you change a container target state's declaration (e.g., add a column to a table schema, change a primary key), CocoIndex detects the change and does its best to alter the target in place. If the change is too large to alter (e.g., changing primary keys), the target is dropped and recreated.

When a target is dropped and recreated, CocoIndex automatically reprocesses all affected components to backfill the data — you don't need to manually trigger `--full-reprocess`. This is handled by the target connector's [child invalidation](../advanced_topics/custom_target_connector#child-invalidation) mechanism, which signals to CocoIndex whether the change is destructive (all children lost) or lossy (some data may be lost).

## Generic target state APIs

For cases where connector-specific APIs don't cover your needs, CocoIndex provides generic APIs:

- `declare_target_state()` — declare a leaf target state
- `declare_target_state_with_child()` — declare a target state that provides child target states

These are exported from `cocoindex` and used internally by connectors. For defining custom targets, see [Custom Target States Connector](../advanced_topics/custom_target_connector).
