Skip to main content
This is the fastest path from a Python file to a live Capsule app. By the end you will have:
  • a local app with chat and a page
  • one small piece of session state
  • a successful hosted deploy
  • a clear sense of where channels, tasks, filesystems, and pricing fit next

1. Create app.py

import cpsl

app = cpsl.App(
    name="hello-capsule",
    image=cpsl.Image(),
)


@app.page("Status", icon="layout-dashboard")
def status_page():
    return cpsl.ui.Page(
        [
            cpsl.ui.Text("Hello from Capsule", style="heading"),
            cpsl.ui.Text(
                "Chat, pages, and deploys all come from the same app definition.",
                style="muted",
            ),
        ]
    )


@app.message()
async def handle(session: cpsl.Session, msg: cpsl.Message):
    session.data["last_message"] = msg.text
    await session.reply(
        f"Hello {session.user.email or 'there'}.\n\n"
        f"Last message saved: {session.data['last_message']}"
    )
This is the functional style, which is the easiest place to start. It is also already a real app surface, not just a prompt handler.
  • App(...) defines the runtime and deploy config
  • image=cpsl.Image() says the app should run in a default Python environment
  • @app.page(...) registers a structured UI page alongside chat
  • @app.message() registers the chat handler
  • session.data stores per-session state for the current app user
  • session.reply() sends a message back to the user
If you are wondering why image= is required here: in functional apps, App(...) needs to know the runtime environment up front. In class-based apps, that same information lives on @app.cls(...) instead.

2. Authenticate once

If you have not used Capsule on this machine before:
capsule login
This connects your local machine to the Capsule workspace so serve and deploy can talk to the platform.

3. Start the live dev loop

capsule serve app:app
app:app means:
  • module: app.py
  • symbol: app
capsule serve uploads the project, boots the runtime, and hot-reloads when you edit watched files such as Python, TSX, CSS, JSON, and Markdown.

4. Use the app

Open the local preview from the capsule serve output. You should see:
  • chat, powered by the @app.message() handler
  • a Status page in the sidebar, powered by @app.page(...)
Send a message in chat and then refresh the page view if you want to confirm that chat and pages belong to the same app definition. If you want to make the state handling more obvious, try this edit:
@app.message()
async def handle(session: cpsl.Session, msg: cpsl.Message):
    session.data.setdefault("messages", []).append(msg.text)

    await session.reply(
        "Stored messages:\n" +
        "\n".join(f"- {text}" for text in session.data["messages"])
    )
Now the app is doing something slightly more useful:
  • session.data stores per-conversation state
  • session.id gives you a stable session identifier
  • saving the file should cause the local preview to pick up the change
At this point you have already seen the core Capsule loop: define an app once, run it in the managed dev runtime, and interact with it through both chat and pages.

5. Deploy it

When the local version looks good:
capsule deploy app:app
Deploy packages the same app definition you just served locally and sends it to Capsule. The deploy output includes the hosted URL for the app. In other words, you are not maintaining a separate deployment config somewhere else. The Python code is the source of truth.

6. Add the next product surface

From here, most apps branch into one or more of these directions:
  • channels for Telegram, Slack, WhatsApp, or API entry points
  • collections for durable data and tables
  • data handlers for computed JSON
  • DSL or React pages for structured UI
  • tasks and schedules for asynchronous work
  • filesystems, integrations, and secrets for external systems
  • pricing when the app becomes something users pay for
A common next step is to add channels or product settings directly on App(...):
app = cpsl.App(
    name="hello-capsule",
    image=cpsl.Image(),
    channels=[cpsl.Channel("my-telegram-bot")],
    price=500,
    pricing_type="monthly",
)
That one change is a good example of how Capsule is meant to work: app behavior, delivery surfaces, and product settings all live in one definition instead of being split across separate systems.

Where to go next