# The CocoIndex App

> **CocoIndex v1, Rust SDK (experimental).** This page documents the CocoIndex **v1** Rust SDK (the `cocoindex` crate). The Rust SDK is experimental: any release may change or remove its API, and it is not yet published on crates.io. The Python SDK pages are at https://cocoindex.io/docs/.
>
> Source: https://cocoindex.io/docs/rust/programming_guide/app/ · Rust docs index: https://cocoindex.io/docs/rust/llms.txt · Rust SDK tour: https://github.com/cocoindex-io/cocoindex/blob/main/rust/sdk/SHOWCASE.md · Rust examples: https://github.com/cocoindex-io/cocoindex/tree/main/examples/rust

**Note — Prerequisite**
Read [Core Concepts](/docs/rust/programming_guide/core_concepts/) for the App and source → transform → target-state model, with diagrams.

An **App** is the top-level runnable unit in CocoIndex.
It names your pipeline and owns its state. When you call `app.run(app_main)` or `app.update(app_main)`, CocoIndex runs the main function as the root [processing component](/docs/rust/programming_guide/processing_component/) which can mount child processing components to do work and declare target states.

## Creating an app

To create an App, provide:

1. **A main function** marked `#[cocoindex::app]` — the entry point for your pipeline
2. **A name** — identifies the pipeline; the function name unless you pass `name = "..."`

```rust
use cocoindex::prelude::*;

#[cocoindex::app(name = "MyPipeline")]
async fn app_main(ctx: Ctx) -> Result<()> {
    index(ctx, source_dir()).await
}

async fn index(ctx: Ctx, sourcedir: PathBuf) -> Result<()> {
    // ... your pipeline logic ...
    Ok(())
}
```

The main function takes the root component's `Ctx` by value and returns a `Result`. `#[cocoindex::app]` registers it under its name, so `Environment::from_registered` and the [`cocoindex` CLI](#managing-apps-with-cli) find it.

Your own code opens the app by name, with the path of its state database:

```rust
let app = App::open("MyPipeline", ".cocoindex_db").await?;
```

`App::open(name, db_path)` is short for `App::builder(name).db_path(db_path).build().await`. When the app needs shared resources, build an `Environment` and open the app from it:

```rust
let app = Environment::builder()
    .db_path(".cocoindex_db")
    .provide_key(&DB, database)
    .build()
    .await?
    .app("MyPipeline")
    .await?;
```

Arguments are not bound to the app. The registered function reads its inputs itself, and when your own code runs the app, a closure passes them (see [Updating an app](#updating-an-app)).

**Tip**
The main function is always an `async fn`. See [Async only](/docs/rust/programming_guide/sdk_overview/#async-only) for details.

## Updating an app

Call `run()` or `update()` with the main function to execute the pipeline:

```rust
// `run` returns the run's statistics.
let stats = app.run(app_main).await?;
println!("{stats}");

// `update` returns the main function's value.
let result = app.update(app_main).await?;

// Arguments travel in a closure.
let stats = app.run(move |ctx| index(ctx, dir)).await?;
```

```rust
// Sync (blocking) API, outside a Tokio runtime
let result = app.update_blocking(app_main)?;
```

**Options** are the fields of `UpdateOptions`. Pass them to `app.update_with_options`, or to `app.start_update_with_options` for a handle:

- `live` keeps the app running after the initial scan so live components can continue watching for changes. See [Live Mode](/docs/rust/programming_guide/live_mode/).
- `report_to_stdout` prints periodic progress updates during execution. `progress_refresh_interval` sets the refresh interval.
- `full_reprocess` reprocesses everything, bypassing all memoization: every component **and** every memoized function re-executes, and all target states are re-applied even if unchanged. Mind the cost — this includes every paid external call (embedding and LLM APIs re-run for all content). To force re-running just one function's work, bump its [`version`](/docs/rust/programming_guide/function/#version) instead.
- `preview` computes the target actions without applying them.
- `timeout` bounds how long the update may run.

```rust
let handle = app.start_update_with_options(
    UpdateOptions {
        full_reprocess: false,
        live: true,
        ..UpdateOptions::default()
    },
    app_main,
)?;
handle.result().await?;
```

`app.preview(app_main).await?` runs with `preview: true` and returns the planned `Vec<PreviewAction>` instead of applying them.

When you update an App, CocoIndex:

1. Uses the environment the lifespans configured (`Environment::from_registered` runs them before it opens the state database)
2. Executes the main function (the root processing component), which mounts child processing components
3. Compares the declared target states with the previous run and applies only the necessary changes to external systems

Given the same logic and inputs, updates are repeatable. When logic or inputs change, only the affected parts re-execute.

To watch progress beyond the `report_to_stdout` option, `app.start_update(app_main)` returns an `UpdateHandle` that exposes stats programmatically: poll with `handle.stats_snapshot()`, wait for a change with `handle.changed().await`, and take the value with `handle.result().await`. See [Progress monitoring](/docs/rust/advanced_topics/progress_monitoring/).

## How an app runs

An App is the top-level runner and entry point. A **processing component** is the unit of incremental execution *within* an app.

- Your app's main function runs as the **root processing component** at the root path.
- Each `mount!` or `use_mount!` (or `ctx.mount`, `ctx.scope`) declares a **child processing component** at a child path. `mount_each!` and the connectors' `mount_*_target` functions also create child components.
- Each processing component declares a set of target states, and CocoIndex syncs them as a unit when that component finishes — all writes happen after processing completes, and each target backend applies its batch atomically when supported.

This is why an update does not "run everything from scratch": CocoIndex uses the component path tree to decide what can be reused and what must re-run.

For example, an app that processes files might mount one component per file — the per-file fan-out from [Core Concepts](/docs/rust/programming_guide/core_concepts/#processing-component), shown here as a path tree:

```text
(root)                         ← app_main component
└── process_file
    ├── "hello.md"             ← process_file component
    └── "world.md"             ← process_file component
```

See [Processing Component](/docs/rust/programming_guide/processing_component/) for how mounting and component paths define these boundaries.

## Database path

CocoIndex needs a database path (`db_path`) to store its internal state. This database tracks target states and memoized results from previous runs, enabling CocoIndex to compute what changed and apply only the necessary updates.

The simplest way to configure the database path is via the `COCOINDEX_DB` environment variable:

```bash
export COCOINDEX_DB=./cocoindex.db
```

With `COCOINDEX_DB` set, you can create and run apps without any additional configuration when the environment comes from `Environment::from_registered()` or the `cocoindex` CLI:

```rust
let env = Environment::from_registered().await?;  // Uses COCOINDEX_DB when no lifespan sets db_path
let app = env.app("MyPipeline").await?;
let stats = app.run(app_main).await?;
env.shutdown().await;
```

A plain `Environment::builder().build()` or `App::builder(name).build()` reads no environment variables: give it the path with `.db_path(..)`.

For details on what the internal database stores and how to tune its LMDB settings (the database grows automatically; `map_size` only sets where it starts), see [Internal Storage](/docs/rust/advanced_topics/internal_storage/).

## Lifespan (optional)

A **lifespan function** defines the CocoIndex runtime lifecycle: its setup runs when the runtime starts (in `Environment::from_registered()`, before the state database opens), and its cleanup runs when the runtime stops. Use it to configure CocoIndex settings programmatically or to initialize shared resources that processing components can reuse.

**Tip**
If you only need to set the database path, using the `COCOINDEX_DB` environment variable is simpler than defining a lifespan function.

### Defining a lifespan

Mark an `async fn` with `#[cocoindex::lifespan]` to register a lifespan function. Every app opened from `Environment::from_registered()` shares the registered lifespans; an app opened from an `Environment` you build yourself is in a different [*Environment*](/docs/rust/advanced_topics/multiple_environments/). The function receives an `EnvironmentBuilder`, configures it, and returns it:

```rust
#[cocoindex::lifespan]
async fn lifespan(builder: EnvironmentBuilder) -> Result<EnvironmentBuilder> {
    let db = postgres::Database::connect(&database_url()).await?;
    let pool = db.pool().clone();
    Ok(builder
        .db_path(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".cocoindex_db"))
        .provide_key(&DB, db)
        .on_shutdown(move || async move { pool.close().await }))
}
```

Register cleanup with `builder.on_shutdown(..)`. The hooks run in reverse order of registration when the environment shuts down. `Environment::from_registered()` and the `cocoindex` CLI run every registered lifespan before they open the state database.

Setting `db_path` in the lifespan takes precedence over the `COCOINDEX_DB` environment variable. If neither is provided, CocoIndex returns an error.

A lifespan is always an `async fn`, and no `yield` splits setup from cleanup: the function returns the builder, and `on_shutdown` hooks do the cleanup.

You can also use the lifespan to provide resources (like database connections) that processing components can access. See [Context](/docs/rust/programming_guide/context/) for details on sharing resources across your pipeline.

### Explicit lifecycle control (optional)

The lifecycle is always explicit: `Environment::from_registered().await?` runs the registered lifespans, and `env.shutdown().await` runs the `on_shutdown` hooks and closes the state database. `#[cocoindex::main]` builds the Tokio runtime and answers `cocoindex ...` CLI invocations before the body runs:

```rust
#[cocoindex::main]
async fn main() -> Result<()> {
    // `cocoindex ...` invocations are handled before this body runs.
    let env = Environment::from_registered().await?;
    let stats = env.app("MyApp").await?.run(app_main).await?;
    println!("{stats}");
    env.shutdown().await;
    Ok(())
}
```

The hooks run once: a second `shutdown` only closes the database. Await every running update or drop before you call `shutdown`.

An `Environment` does not restart after `shutdown`: its state database is closed. To run again, build a new environment with `Environment::from_registered()`. Persisted memoization and target-state tracking in the state database carry over to it.

## Managing apps with CLI

CocoIndex provides a CLI for managing your apps without writing additional code.

The `cocoindex` binary from `rust/cli` builds your crate and drives its binary. The crate's `main` must carry `#[cocoindex::main]`, and each app must carry `#[cocoindex::app]`. The CLI's app target is `[path][:app_name]`: a crate directory (default `.`), a `Cargo.toml`, or a built binary, with the app's name after a colon when the crate registers more than one app.

### Update an app

Run your app once to sync all target states:

```bash
cocoindex update            # the crate in the current directory
cocoindex update :MyApp     # one app of a crate that registers several
```

This executes your pipeline and applies all declared target states to external systems. Add `--live` (or `-L`) to keep the app running and react to source changes continuously — see [Live Mode](/docs/rust/programming_guide/live_mode/).

### Drop an app

Remove an app and revert all its target states:

```bash
cocoindex drop
```

From code, `app.drop_state().await?` does the same for one `App`.

This will delete all target states created by the app (e.g., drop tables, delete rows) and clear its internal state.

`drop` is an explicit, foreground operation — any failure during the recursive delete (root or any descendant) returns an error rather than being silently logged. The internal tracking record for a component whose delete failed is preserved so the next `drop` (with the underlying problem fixed) can complete the cleanup. See [Error Handling](/docs/rust/advanced_topics/exception_handlers/) for the general principle.

See [CLI Reference](/docs/rust/cli/) for more commands and options.
