Skip to main content
Pages get much more useful once they stop being static. In Capsule, the usual pattern is:
  1. declare @app.data(...) functions that return JSON
  2. render the results in @app.page(...) with cpsl.ui
  3. add settings so users can tune behavior without editing code
That gives you a nice separation of concerns:
  • Python functions produce data
  • pages decide how to show it
  • settings let users control behavior without changing the code

1. Declare a few settings

Settings are scoped key-value entries stored for the app, owner, or user.
import cpsl

app = cpsl.App(name="dashboard-demo", image=cpsl.Image())

app.setting("daily_goal", scope="user", type=int, default=3, label="Daily goal")
app.setting("show_completed", scope="user", type=bool, default=True, label="Show completed")
app.setting(
    "default_view",
    scope="user",
    type=str,
    default="table",
    options=["table", "chart"],
    label="Default view",
)
app.setting("summary_prompt", scope="user", type=str, default="Short and useful")

2. Add data handlers

Data handlers register JSON endpoints at GET /data/<name>.
@app.data("stats", access="authenticated")
def get_stats(ctx: cpsl.RequestContext):
    return {
        "viewer": ctx.user.email,
        "open_tasks": 12,
        "completed_today": 4,
        "health": 0.97,
    }


@app.data("activity", access="authenticated")
def get_activity(period: str = "7d"):
    return [
        {"day": "Mon", "count": 3},
        {"day": "Tue", "count": 5},
        {"day": "Wed", "count": 4},
        {"day": "Thu", "count": 7},
        {"day": "Fri", "count": 2},
    ]


@app.data("recent_tasks", access="authenticated")
def get_recent_tasks():
    return [
        {"title": "Import leads", "status": "done", "owner": "Ada"},
        {"title": "Generate report", "status": "running", "owner": "Ibrahim"},
        {"title": "Review findings", "status": "queued", "owner": "Mina"},
    ]
If you want to picture how these are exposed, @app.data("activity") becomes a JSON endpoint at GET /data/activity. Function parameters are treated like query parameters, so a call like this:
/data/activity?period=30d
would call:
get_activity(period="30d")
Important rules:
  • handlers must return JSON-serializable values
  • access="public" makes the endpoint visible without login
  • access="authenticated" requires an authenticated user
  • function parameters become query parameters, as in period="7d"
  • RequestContext is optional, but useful when you need the caller’s identity or integrations

3. Build a Python DSL page

Now render the data:
@app.page("Overview", icon="layout-dashboard", access="authenticated")
def overview():
    return cpsl.ui.Page(
        [
            cpsl.ui.Row(
                [
                    cpsl.ui.Metric("Open Tasks", data="stats", field="open_tasks"),
                    cpsl.ui.Metric("Completed Today", data="stats", field="completed_today"),
                    cpsl.ui.Metric(
                        "Health",
                        data="stats",
                        field="health",
                        format="percent",
                    ),
                ]
            ),
            cpsl.ui.Divider(),
            cpsl.ui.Chart(
                data="activity",
                chart_type="bar",
                x="day",
                y="count",
            ),
            cpsl.ui.Divider(),
            cpsl.ui.Table(
                data="recent_tasks",
                columns=["title", "status", "owner"],
            ),
        ]
    )
The important thing to notice is that the page does not fetch anything itself. You bind widgets to named data sources and Capsule wires the page to those handlers for you. That is why DSL pages stay compact even when the UI starts getting more interesting.

4. Add a settings page

UI widgets like Toggle, TextInput, NumberInput, and Select bind directly to declared settings:
@app.page("Preferences", icon="sliders-horizontal", access="authenticated")
def preferences():
    return cpsl.ui.Page(
        [
            cpsl.ui.Text("Preferences", style="heading"),
            cpsl.ui.Text("These controls read and write user-scoped settings.", style="muted"),
            cpsl.ui.Divider(),
            cpsl.ui.Card(
                "Display",
                [
                    cpsl.ui.Toggle("Show completed items", setting="show_completed"),
                    cpsl.ui.NumberInput("Daily goal", setting="daily_goal", min=1, max=20, step=1),
                    cpsl.ui.Select("Default view", setting="default_view"),
                ],
            ),
            cpsl.ui.Card(
                "Prompting",
                [
                    cpsl.ui.TextInput(
                        "Summary prompt",
                        setting="summary_prompt",
                        multiline=True,
                        placeholder="How should the app summarize results?",
                    ),
                ],
            ),
        ]
    )
These widgets are especially handy for “operator settings” pages where you want the app to stay configurable but do not want to build a separate form system.

5. Read settings in Python

Settings are not only for UI. You can read and write them inside handlers:
@app.message()
async def handle(session: cpsl.Session, msg: cpsl.Message):
    goal = await app.settings.get("daily_goal")
    prompt = await app.settings.get("summary_prompt")
    await session.reply(f"Daily goal: {goal}\nSummary prompt: {prompt}")
That same accessor works anywhere inside the app runtime, so it is common to use settings to control prompts, filters, toggles, and page defaults. You can also write them yourself:
await app.settings.set("daily_goal", 5)
For example, you might let a user update a setting from chat:
@app.message()
async def handle(session: cpsl.Session, msg: cpsl.Message):
    text = msg.text.strip()

    if text.startswith("set goal "):
        value = int(text.split()[-1])
        await app.settings.set("daily_goal", value)
        await session.reply(f"Daily goal updated to {value}")
        return

6. Full example

import cpsl

app = cpsl.App(name="dashboard-demo", image=cpsl.Image())

app.setting("daily_goal", scope="user", type=int, default=3, label="Daily goal")
app.setting("show_completed", scope="user", type=bool, default=True, label="Show completed")
app.setting(
    "default_view",
    scope="user",
    type=str,
    default="table",
    options=["table", "chart"],
    label="Default view",
)
app.setting("summary_prompt", scope="user", type=str, default="Short and useful")


@app.data("stats", access="authenticated")
def get_stats(ctx: cpsl.RequestContext):
    return {
        "viewer": ctx.user.email,
        "open_tasks": 12,
        "completed_today": 4,
        "health": 0.97,
    }


@app.data("activity", access="authenticated")
def get_activity(period: str = "7d"):
    return [
        {"day": "Mon", "count": 3},
        {"day": "Tue", "count": 5},
        {"day": "Wed", "count": 4},
        {"day": "Thu", "count": 7},
        {"day": "Fri", "count": 2},
    ]


@app.data("recent_tasks", access="authenticated")
def get_recent_tasks():
    return [
        {"title": "Import leads", "status": "done", "owner": "Ada"},
        {"title": "Generate report", "status": "running", "owner": "Ibrahim"},
        {"title": "Review findings", "status": "queued", "owner": "Mina"},
    ]


@app.page("Overview", icon="layout-dashboard", access="authenticated")
def overview():
    return cpsl.ui.Page(
        [
            cpsl.ui.Row(
                [
                    cpsl.ui.Metric("Open Tasks", data="stats", field="open_tasks"),
                    cpsl.ui.Metric("Completed Today", data="stats", field="completed_today"),
                    cpsl.ui.Metric("Health", data="stats", field="health", format="percent"),
                ]
            ),
            cpsl.ui.Divider(),
            cpsl.ui.Chart(data="activity", chart_type="bar", x="day", y="count"),
            cpsl.ui.Divider(),
            cpsl.ui.Table(data="recent_tasks", columns=["title", "status", "owner"]),
        ]
    )


@app.page("Preferences", icon="sliders-horizontal", access="authenticated")
def preferences():
    return cpsl.ui.Page(
        [
            cpsl.ui.Card(
                "Display",
                [
                    cpsl.ui.Toggle("Show completed items", setting="show_completed"),
                    cpsl.ui.NumberInput("Daily goal", setting="daily_goal", min=1, max=20, step=1),
                    cpsl.ui.Select("Default view", setting="default_view"),
                ],
            ),
            cpsl.ui.Card(
                "Prompting",
                [
                    cpsl.ui.TextInput(
                        "Summary prompt",
                        setting="summary_prompt",
                        multiline=True,
                        placeholder="How should the app summarize results?",
                    ),
                ],
            ),
        ]
    )

Next steps