The CocoIndex App
Apps are the top-level runnable unit in CocoIndex. Learn how to create an App, trigger updates, configure the database path, and wire up lifespan-managed resources.
Read 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 which can mount child processing components to do work and declare target states.
Creating an app
To create an App, provide:
- A main function marked
#[cocoindex::app]— the entry point for your pipeline - A name — identifies the pipeline; the function name unless you pass
name = "..."
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 find it.
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:
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).
The main function is always an async fn. See Async only for details.
Updating an app
Callrun() or update() with the main function to execute the pipeline:
// `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?;// 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:
livekeeps the app running after the initial scan so live components can continue watching for changes. See Live Mode.report_to_stdoutprints periodic progress updates during execution.progress_refresh_intervalsets the refresh interval.full_reprocessreprocesses 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 itsversioninstead.previewcomputes the target actions without applying them.timeoutbounds how long the update may run.
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:
-
Uses the environment the lifespans configured (
Environment::from_registeredruns them before it opens the state database) - Executes the main function (the root processing component), which mounts child processing components
- 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 thereport_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.
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!oruse_mount!(orctx.mount,ctx.scope) declares a child processing component at a child path.mount_each!and the connectors’mount_*_targetfunctions 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, shown here as a path tree:
(root) ← app_main component
└── process_file
├── "hello.md" ← process_file component
└── "world.md" ← process_file component
See 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:
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:
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.
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.
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 anasync 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. The function receives an EnvironmentBuilder, configures it, and returns it:
#[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 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:
#[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:
cocoindex update # the crate in the current directory
cocoindex update :MyApp # one app of a crate that registers severalThis 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.
Drop an app
Remove an app and revert all its target states:
cocoindex dropFrom 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 for the general principle.
See CLI Reference for more commands and options.