Skip to main content
cpsl.App is the root object for a Capsule application. The shortest useful way to think about it is this: App is both the place where you describe the product and the object Capsule reads when it serves or deploys that product. Pages, collections, integrations, and deploy knobs all live on the same object for that reason.

Worked example

This is the kind of App definition a real product grows into:
import cpsl
import cpsl.ui as ui
from openai import AsyncOpenAI

client = AsyncOpenAI()

app = cpsl.App(
    name="support-ops",
    image=cpsl.Image(python_packages=["openai"]),
    channels=[cpsl.Channel("support-telegram")],
    keep_warm_seconds=30,
    cpu=0.5,
    memory=1024,
    secrets=["OPENAI_API_KEY"],
    filesystems={"/data": cpsl.FileSystem("support-ops")},
    price=1500,
    pricing_type="monthly",
)

app.add_integration(
    cpsl.Gmail(
        client_id=cpsl.Secret.from_name("GOOGLE_OAUTH_CLIENT_ID"),
        client_secret=cpsl.Secret.from_name("GOOGLE_OAUTH_CLIENT_SECRET"),
        scopes=["https://www.googleapis.com/auth/gmail.readonly"],
    )
)

threads = app.collection(
    "threads",
    columns=["subject", "status", "priority", "thread_id"],
    scope="owner",
    sortable=True,
    filterable=True,
)

app.setting("auto_sync", scope="owner", type=bool, default=True)


@app.data("mailbox_metrics", access="authenticated")
async def mailbox_metrics():
    rows = await threads.find(limit=500)
    return {"total_threads": len(rows)}


@app.page("Overview", icon="layout-dashboard")
def overview():
    return ui.Page([ui.Metric("Threads", data="mailbox_metrics", field="total_threads")])


@app.task()
async def sync_threads(session: cpsl.Session | None = None):
    if session:
        await session.notify("Syncing mailbox state...")
    await threads.insert_one(
        {
            "subject": "Renewal review",
            "status": "needs-review",
            "priority": "high",
            "thread_id": "thread_123",
        }
    )


@app.message()
async def handle(session: cpsl.Session, msg: cpsl.Message):
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "You are a support operations copilot."},
            *session.chat_messages(msg),
        ],
    )
    await session.reply(response.choices[0].message.content or "No response.")
You do not need everything at once, but this example shows why App is the center of Capsule. Runtime, pages, state, integrations, tasks, channels, and pricing all hang off the same object.

Constructor

app = cpsl.App(
    name="my-app",
    image=cpsl.Image(python_packages=["openai"]),
    channels=[cpsl.Channel("my-telegram-bot")],
    keep_warm_seconds=30,
    cpu=0.25,
    memory=512,
    secrets=["OPENAI_API_KEY"],
    filesystems={"/data": cpsl.FileSystem("reports")},
    price=500,
    pricing_type="monthly",
)
You do not need all of these fields on every app. A small app often starts as just:
app = cpsl.App(
    name="hello",
    image=cpsl.Image(),
)

Constructor arguments

ArgumentMeaning
nameApp name used for deploys and lookups
imageRuntime image spec. Required for functional apps
channelsExternal transport declarations. Web chat is implicit
keep_warm_secondsKeep the runtime warm after activity
cpuNumber of vCPUs allocated to the sandbox
memoryMiB of RAM allocated to the sandbox
secretsWorkspace secret names to inject into the runtime
filesystemsMount-path to FileSystem mapping
pricePaid app access price in cents
pricing_type"one_time" or "monthly"

App-building methods

Most of the App API is declarative. You call these while the module is imported, and Capsule remembers the resulting configuration. The main question is usually not “how do I call this?” but “which part of the app should own this concern?”

app.collection(...)

Declare a persistent collection and get back a CollectionRef you can use from handlers.
contacts = app.collection(
    "contacts",
    columns=[cpsl.Column("email", type="email")],
    scope="app",
    sortable=True,
    filterable=True,
    paginate=20,
)
This is usually the first thing to reach for when the app needs real persistent records.

app.setting(...)

Declare a scoped setting that can be bound to UI widgets or read from Python.
app.setting("daily_goal", scope="user", type=int, default=3)
Settings are better for configuration than for records. Think “user preference”, not “business object”.

app.theme(...)

Configure colors, fonts, tagline, logo, and preset.
app.theme(preset="midnight", accent="#A78BFA", font_sans="DM Sans, sans-serif")
In practice, most apps call this once near the top of the file and rarely touch it again.

app.add_integration(...)

Declare a user-facing integration.
app.add_integration(cpsl.AWS())
app.add_integration(
    cpsl.GitHub(
        client_id=cpsl.Secret.from_name("GITHUB_CLIENT_ID"),
        client_secret=cpsl.Secret.from_name("GITHUB_CLIENT_SECRET"),
        scopes=["repo"],
    )
)
If the integration belongs to the end user, declare it here. If the credential belongs to the app itself, that is usually a workspace secret instead.

app.add_page(...)

Register a React/TSX page.
app.add_page(
    "Dashboard",
    icon="layout-dashboard",
    component="pages/dashboard.tsx",
    packages=["lightweight-charts@4.2.0"],
    access="authenticated",
)
Use this when the page needs richer interaction or external frontend libraries.

app.page(...)

Decorator for Python DSL pages. The function must return cpsl.ui.Page.
@app.page("Overview", icon="chart-bar")
def overview():
    return cpsl.ui.Page([...])
Use this when the page is mostly a composition of metrics, tables, charts, and settings controls.

app.workflow(...)

Register a named workflow surface.
wf = app.workflow(
    "Research Company",
    icon="search",
    description="Collect context and draft a brief.",
    scope="user",
)
Use workflows when users should start a named flow from the sidebar and continue inside a session-backed chat. See Workflows for launcher UI, start handlers, actions, and message handlers.

app.data(...)

Register a named JSON data source.
@app.data("stats", access="authenticated")
def get_stats():
    return {"open_tasks": 12}
This is the cleanest way to expose computed data to both DSL pages and React pages.

Functional-only methods

These are only valid when the app was created with image=... on the constructor:
  • app.boot()
  • app.shutdown()
  • app.enter()
  • app.exit()
  • app.message()
  • app.schedule(cron)
  • app.endpoint(...)
  • app.asgi(...)
  • app.task(...)
Here is a small functional app that uses several of them together:
import cpsl
from openai import AsyncOpenAI

client = AsyncOpenAI()

app = cpsl.App(
    name="ops-bot",
    image=cpsl.Image(python_packages=["openai"]),
    secrets=["OPENAI_API_KEY"],
)


@app.boot()
async def on_boot():
    print("booted")


@app.message()
async def handle(session: cpsl.Session, msg: cpsl.Message):
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "You are a concise operations copilot."},
            *session.chat_messages(msg),
        ],
    )
    await session.reply(response.choices[0].message.content or "No response.")


@app.task()
async def sync_index():
    ...

Class-based apps

For class-based apps, use @app.cls(...) and the global decorators from cpsl:
from openai import AsyncOpenAI

app = cpsl.App(name="my-app")


@app.cls(image=cpsl.Image(python_packages=["openai"]), secrets=["OPENAI_API_KEY"])
class MyApp:
    @cpsl.boot()
    def setup(self):
        self.client = AsyncOpenAI()

    @cpsl.message()
    async def handle(self, session: cpsl.Session, msg: cpsl.Message):
        response = await self.client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": "You are a concise Capsule assistant."},
                *session.chat_messages(msg),
            ],
        )
        await session.reply(response.choices[0].message.content or "No response.")

@app.cls(...) arguments

ArgumentMeaning
imageRuntime image spec
pricePaid app access price in cents
pricing_type"one_time" or "monthly"
channelsExternal channels
keep_warm_secondsWarm runtime window
cpuNumber of vCPUs allocated to the sandbox
memoryMiB of RAM allocated to the sandbox
secretsSecret names to inject
filesystemsMount-path to FileSystem mapping

app.settings

Every App has a settings accessor:
  • await app.settings.get(key)
  • await app.settings.set(key, value)
  • await app.settings.get_all(keys=None)
Example:
goal = await app.settings.get("daily_goal")
await app.settings.set("daily_goal", goal + 1)

See also