Skip to main content
Capsule apps usually need three different kinds of external state, and Capsule models all three directly in the platform:
  • workspace secrets for app-owned credentials
  • user integrations for end-user OAuth or secret-based connections
  • filesystems for durable mounted storage inside the runtime
This tutorial shows how those pieces fit together so the app can talk to external systems, store files, and gather user credentials without inventing a separate storage or auth layer around the agent.

1. Create the external resources

Before wiring the app, create the resources in your workspace:
capsule secret create OPENAI_API_KEY=sk-... GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=...
capsule fs create reports
If you want a named external channel such as Telegram or Slack, create that separately with capsule channel create. Channels are covered in the reference section.

2. Declare the app config

import cpsl

app = cpsl.App(
    name="integrations-demo",
    image=cpsl.Image(),
    secrets=["OPENAI_API_KEY"],
    filesystems={"/data": cpsl.FileSystem("reports")},
)
This does two things:
  • secrets=["OPENAI_API_KEY"] injects that secret into the runtime environment
  • filesystems={"/data": ...} mounts the named filesystem at /data
Inside the runtime you can read the mounted directory like any normal path.

3. Declare integrations

Capsule supports both OAuth-style and secret-form integrations.

OAuth integration

app.add_integration(
    "github",
    client_id=cpsl.Secret.from_name("GITHUB_CLIENT_ID"),
    client_secret=cpsl.Secret.from_name("GITHUB_CLIENT_SECRET"),
    scopes=["repo", "read:user"],
)

Secret-based integration

app.add_integration("aws")
Known secret integrations such as aws and tailscale infer their required fields automatically.

4. Prompt the user at runtime

@app.message()
async def handle(session: cpsl.Session, msg: cpsl.Message):
    text = (msg.text or "").strip().lower()

    if text == "connect github":
        cred = await session.prompt_integration(
            "github",
            reason="Need GitHub access to inspect repositories",
        )
        await session.reply(f"Connected github with scopes: {cred.scopes}")
        return

    if text == "connect aws":
        cred = await session.prompt_integration(
            "aws",
            reason="Need AWS credentials to inspect infrastructure",
        )
        await session.reply(f"Connected aws for region {cred.fields.get('region')}")
        return

    if text == "hint github":
        await session.show_integration(
            "github",
            reason="Connect GitHub to unlock repository analysis",
        )
        await session.reply("Rendered a non-blocking integration card.")
        return
Use:
  • prompt_integration() when the handler must block until credentials exist
  • show_integration() when the integration is optional and you only want to nudge the user

5. Read secrets safely

When a secret is app-owned rather than user-owned, read it with Secret.from_name(...).value:
api_key = cpsl.Secret.from_name("OPENAI_API_KEY").value
The lookup checks the environment first, then falls back to the runtime resolver if needed.

6. Work with mounted filesystems

Mounted filesystems behave like normal directories inside the app:
import os


@app.boot()
async def on_boot():
    os.makedirs("/data/reports", exist_ok=True)
Then write files in handlers or tasks:
with open("/data/reports/summary.txt", "w") as f:
    f.write("Capsule generated this report.\n")
Use filesystems when the data should outlive one request or one chat session.

7. Accept user uploads

If the user needs to provide a file, use prompt_file():
    if text == "upload csv":
        upload = await session.prompt_file(
            message="Upload a CSV file",
            accept=".csv,text/csv",
            path="/data/uploads",
        )
        await session.reply(f"Uploaded {upload.name} to {upload.path}")
        return
The returned FileUpload contains metadata plus a download() helper if you want to save it somewhere else manually.

Full example

import os
import cpsl

app = cpsl.App(
    name="integrations-demo",
    image=cpsl.Image(),
    secrets=["OPENAI_API_KEY"],
    filesystems={"/data": cpsl.FileSystem("reports")},
)

app.add_integration(
    "github",
    client_id=cpsl.Secret.from_name("GITHUB_CLIENT_ID"),
    client_secret=cpsl.Secret.from_name("GITHUB_CLIENT_SECRET"),
    scopes=["repo", "read:user"],
)
app.add_integration("aws")


@app.boot()
async def on_boot():
    os.makedirs("/data/reports", exist_ok=True)


@app.message()
async def handle(session: cpsl.Session, msg: cpsl.Message):
    text = (msg.text or "").strip().lower()

    if text == "connect github":
        cred = await session.prompt_integration(
            "github",
            reason="Need GitHub access to inspect repositories",
        )
        await session.reply(f"Connected github with scopes: {cred.scopes}")
        return

    if text == "connect aws":
        cred = await session.prompt_integration(
            "aws",
            reason="Need AWS credentials to inspect infrastructure",
        )
        await session.reply(f"Connected aws for region {cred.fields.get('region')}")
        return

    if text == "hint github":
        await session.show_integration(
            "github",
            reason="Connect GitHub to unlock repository analysis",
        )
        await session.reply("Rendered a non-blocking integration card.")
        return

    if text == "upload csv":
        upload = await session.prompt_file(
            message="Upload a CSV file",
            accept=".csv,text/csv",
            path="/data/uploads",
        )
        await session.reply(f"Uploaded {upload.name} to {upload.path}")
        return

    if text == "write report":
        api_key = cpsl.Secret.from_name("OPENAI_API_KEY").value
        with open("/data/reports/summary.txt", "w") as f:
            f.write(f"Secret loaded: {bool(api_key)}\n")
        await session.reply("Wrote /data/reports/summary.txt")
        return

    await session.reply(
        "Try: connect github, connect aws, hint github, upload csv, or write report"
    )

Next steps