Skip to main content
Capsule supports two authoring styles. They are not two different runtimes or two different product models. They are just two ways of organizing the same app. The real choice is mostly about code organization and whether instance state on self helps. Capsule supports:
  • functional: handlers are registered directly on app
  • class-based: handlers live on a class decorated with @app.cls(...)
Both compile down to the same runtime concepts. The important rule is that you do not mix them in one app.

Functional style

Functional apps declare the runtime at App(...) construction time:
import cpsl
from openai import AsyncOpenAI

client = AsyncOpenAI()

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


@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 Capsule assistant."},
            *session.chat_messages(msg),
        ],
    )
    await session.reply(response.choices[0].message.content or "No response.")
Use the functional style when:
  • you want the smallest, clearest app shape
  • most logic is stateless or organized into helper functions
  • you prefer top-level decorators like @app.message() and @app.task()

Class-based style

Class-based apps put handlers on a runtime class:
import cpsl
from openai import AsyncOpenAI

app = cpsl.App(name="hello")


@app.cls(image=cpsl.Image(python_packages=["openai"]), secrets=["OPENAI_API_KEY"])
class HelloApp:
    @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.")
Use the class-based style when:
  • instance state on self is helpful
  • you want a more object-oriented organization
  • you prefer class-local boot/message/task methods

Do not mix the styles

These combinations are invalid:
  • App(..., image=...) plus @app.cls(...)
  • @app.message() plus class methods decorated with @cpsl.message()
Pick one style per app.

Decorator mapping

FunctionalClass-based
@app.boot()@cpsl.boot()
@app.shutdown()@cpsl.shutdown()
@app.enter()@cpsl.enter()
@app.exit()@cpsl.exit()
@app.message()@cpsl.message()
@app.task()@cpsl.task()
@app.schedule()@cpsl.schedule()
@app.endpoint()@cpsl.endpoint()
@app.asgi()@cpsl.asgi()

Shared features across both styles

No matter which style you pick, these APIs stay the same:
  • app.collection(...)
  • app.setting(...)
  • app.theme(...)
  • app.add_page(...)
  • app.page(...)
  • app.data(...)
  • app.add_integration(...)
Those declarations happen on app, not on the class.

Choosing a default

For most new docs and examples, the functional style is the best default because:
  • it is shorter
  • it makes the deploy config explicit on App(...)
  • it reads well in tutorials
The class-based style is still useful and fully supported, especially for apps that benefit from instance methods and mutable runtime state.