Skip to main content
These three concepts are easy to mix up at first:
  • channels decide how messages enter the app
  • integrations are end-user connections the app can ask for at runtime
  • secrets are app-owned credentials

Worked example

Here is what they look like when combined in one app:
app = cpsl.App(
    name="support-ops",
    image=cpsl.Image(python_packages=["openai"]),
    channels=[cpsl.Channel("support-telegram")],
    secrets=["OPENAI_API_KEY"],
)

app.add_integration(
    "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"],
)
That app can:
  • answer in built-in chat and Telegram because of the channel binding
  • read the operator’s model key because of the secret injection
  • ask each end user to connect Gmail when the product needs mailbox access

Channels

Channel / ChannelRef

cpsl.Channel("name") references a named channel resource created through the CLI or dashboard.
channels=[cpsl.Channel("my-telegram-bot")]
This is the preferred way to attach Telegram, Slack, or WhatsApp to an app.

Chat

cpsl.Chat() represents the built-in web chat. It exists mainly for backward compatibility because web chat is enabled implicitly.

API

cpsl.API() represents synchronous request/response access.

Deprecated inline channel classes

These still exist but are deprecated:
  • cpsl.Telegram(...)
  • cpsl.Slack(...)
  • cpsl.WhatsApp(...)
Prefer named channel resources plus cpsl.Channel("name").

Integrations

Declare integrations with app.add_integration(...).

OAuth integrations

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

Secret-based integrations

app.add_integration("aws")
Known secret integrations include:
  • aws
  • tailscale
You can also declare a custom secret-form integration by passing fields=[...]. Reading an integration at runtime usually looks like this:
@app.data("threads", access="authenticated")
async def get_threads(ctx: cpsl.RequestContext):
    gmail = ctx.integrations.get("gmail")
    if not gmail:
        return {"threads": [], "gmail_connected": False}

    return {"threads": await threads.find(limit=50), "gmail_connected": True}

IntegrationCredentials

At runtime, connected credentials appear on session.integrations.

Fields

FieldMeaning
access_tokenOAuth bearer token or empty for secret-form integrations
token_typeUsually "Bearer"
scopesGranted scopes
expires_atExpiry timestamp when available
fieldsSubmitted secret-form fields such as AWS keys

Secret API

Secret.from_name(name)

Creates a secret reference.
secret = cpsl.Secret.from_name("OPENAI_API_KEY")

secret.value

Resolves the runtime value:
api_key = cpsl.Secret.from_name("OPENAI_API_KEY").value
Capsule checks the environment first, then uses the runtime resolver if necessary. That means this works both when the secret was injected directly into the runtime and when Capsule needs to fetch it through the runtime connection. Use secret references for:
  • app-owned API keys
  • OAuth client credentials
  • channel credentials

Prompting helpers

These are the most important runtime entry points:
  • await session.prompt_integration(type, reason="", timeout=...)
  • await session.show_integration(type, reason="")
Use the first when the current flow cannot continue without the credential. Use the second when the connection is optional. Example of the non-blocking variant:
await session.show_integration(
    "github",
    reason="Connect GitHub to unlock repository analysis",
)

See also