Skip to main content
If your app needs to do real work beyond one chat reply, this is where that work belongs. Capsule has a built-in task execution model, so you do not need a separate worker service just to run background jobs or recurring automation. Tasks and schedules let Capsule apps keep the chat loop responsive while still doing real work.

Why this is a platform feature

In Capsule, background execution is part of the same app model as chat, pages, and deploys. That means:
  • tasks can share the same collections, settings, filesystems, and integrations as the rest of the app
  • the UI can render task state directly with task cards and task boards
  • you do not need to split the app across a request stack and a second job stack before the product is even useful

Tasks

Tasks are declared with @app.task() or @cpsl.task(). They are useful for:
  • slow API calls
  • report generation
  • batch processing
  • retries and cancellation
  • work that should continue after the current reply
Tasks return TaskHandle objects from .submit() and .schedule().

What belongs in a task?

As a rule of thumb, put work in a task when any of these are true:
  • it talks to an external system
  • it might take more than a quick request-response turn
  • it should retry safely
  • the user should be able to leave the page while it keeps running
  • the result should show up later in collections or a workflow page
Good examples are inbox syncs, report generation, attachment processing, enrichment jobs, or any state transition with real side effects.

Schedules

Schedules are declared with @app.schedule(cron) or @cpsl.schedule(cron). Use them for:
  • recurring syncs
  • periodic cleanup
  • digests
  • precomputation and cache warming
Schedules run on cron, not in response to a live chat message.

Task UI

Capsule has first-class task UI:
  • session.show_task(handle) renders an inline task card
  • cpsl.ui.TaskBoard(...) renders a full-page kanban view
That means background execution is part of the product surface, not just an implementation detail.

How background work stays observable

Capsule gives you a few ways to make async work visible to the user:
  • session.show_task(handle) for inline task cards from chat
  • TaskBoard(...) for page-level visibility
  • collections and data handlers for richer audit trails or workflow state
That combination matters. Real apps need users to trust that work was started, is still running, and either completed or failed for a reason.

Runtime context

Tasks can optionally receive a session:
  • with a bound session, the task can reply, notify, stream, and access session.db
  • without one, the task still runs, but there is no live chat target
Scheduled handlers usually rely on cpsl.current_session() when they need owner-scoped runtime identity.

Process isolation

Set process=True when the task should run in a separate OS process for:
  • CPU parallelism
  • crash isolation
  • heavy local computation

Locking

Task lock= values prevent duplicate work for the same logical key:
@app.task(lock="report:{report_id}")
async def build_report(...):
    ...
This is especially useful for idempotent jobs, recurring syncs, and user-triggered retries.

How background work stays safe

The safety model is a mix of ordinary engineering and Capsule primitives:
  • use lock= when duplicate work would be harmful
  • use retries= when a task should self-heal from transient failures
  • keep the side-effecting code inside the task instead of the chat turn
  • write the resulting state back to collections so pages and later turns can inspect it
The model should help decide what to do. The task should own doing it safely.