Skip to main content
Collections and settings are easy to mix up at first because they both store data. The difference is simpler than it sounds. Collections hold the records your app works on. Settings hold the knobs people use to control how the app behaves. Once that distinction clicks, the rest of the page reads much more naturally.

Collection scopes

Collections and settings are related, but they solve different problems:
  • collections are for records
  • settings are for configuration

Worked example

This is a common production shape:
threads = app.collection(
    "threads",
    columns=[
        cpsl.Column("subject"),
        cpsl.Column("sender", type="email"),
        cpsl.Column("status", type="status"),
        cpsl.Column("priority"),
    ],
    scope="owner",
    sortable=True,
    filterable=True,
    paginate=25,
)

app.setting("show_closed_threads", scope="owner", type=bool, default=False)
app.setting(
    "default_tab",
    scope="owner",
    type=str,
    default="all",
    options=["all", "needs-review", "waiting", "closed"],
)
Here the collection holds the workflow records and the settings hold operator preferences. That is the split most apps want. Capsule collections support four scopes:
ScopeMeaning
appShared across the whole app
userIsolated per authenticated app user
ownerShared across an owner or organization
sessionIsolated to a single session
A practical way to read the scopes:
  • app for a shared lead table
  • user for saved bookmarks for one user of this app
  • owner for organization-wide reports
  • session for temporary scratch notes

cpsl.Column

Use cpsl.Column when the table needs more than a bare field name. It lets you tell Capsule how a column should be understood and rendered.
cpsl.Column("status", type="status")

Supported column types

  • text
  • number
  • currency
  • date
  • link
  • email
  • status
  • tags
  • boolean

Column fields

FieldMeaning
keyStored field name
typeColumn type
labelOptional display label
formatOptional display formatting hint
Example collection declaration:
contacts = app.collection(
    "contacts",
    columns=[
        cpsl.Column("name"),
        cpsl.Column("email", type="email"),
        cpsl.Column("website", type="link"),
        cpsl.Column("status", type="status"),
    ],
    scope="app",
    sortable=True,
    filterable=True,
    paginate=20,
)

CollectionRef

CollectionRef is returned by app.collection(...).

Common methods

MethodPurpose
insert_one(document)Insert one document
insert_many(documents)Insert multiple documents
find_one(filter=None)Find one document
find(filter=None, limit=0, skip=0, sort=None)Query many documents
update_one(filter, update)Patch one document
delete_one(filter)Delete one document
count(filter=None)Count matching documents
Inserted documents get both _id and id. Typical CRUD flow:
created = await threads.insert_one(
    {
        "subject": "Renewal review",
        "sender": "ops@example.com",
        "status": "needs-review",
        "priority": "high",
    }
)

row = await threads.find_one({"_id": created["_id"]})
rows = await threads.find({"status": "needs-review"}, limit=25, sort={"priority": 1})
count = await threads.count()

Update behavior

Plain update dicts are treated as patches and automatically wrapped in $set:
await threads.update_one({"_id": thread_id}, {"status": "waiting"})
Operator documents pass through unchanged:
await threads.update_one({"_id": thread_id}, {"$inc": {"review_count": 1}})
Do not mix plain keys and operator keys in one update document.

session.db

Inside a live session, session.db gives you a scoped database proxy:
await session.db.bookmarks.insert_one({"title": "Capsule"})
This is especially convenient for user, owner, and session collections, where the live session already knows which scoped database it should be talking to. Example:
@app.message()
async def handle(session: cpsl.Session, msg: cpsl.Message):
    await session.db.review_notes.insert_one({"note": msg.text})
    total = await session.db.review_notes.count()
    await session.reply(f"Review notes in this scope: {total}")

Settings declarations

Declare settings with app.setting(...):
app.setting("daily_goal", scope="user", type=int, default=3)

Arguments

ArgumentMeaning
nameUnique setting key
scopeapp, owner, or user
typebool, str, int, or float
defaultDefault value when unset
optionsAllowed values for select-like settings
labelHuman-readable UI label
A fuller example:
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"],
)

app.settings

The settings accessor methods are async:
value = await app.settings.get("daily_goal")
await app.settings.set("daily_goal", 5)
all_values = await app.settings.get_all()
Settings are stored in a reserved backing collection and scoped using the same identity rules as collections. In practice, you mostly notice this because settings and pages fit together cleanly:
@app.page("Preferences")
def preferences():
    return cpsl.ui.Page(
        [
            cpsl.ui.Toggle("Show completed", setting="show_completed"),
            cpsl.ui.NumberInput("Daily goal", setting="daily_goal", min=1, max=20),
            cpsl.ui.Select("Default view", setting="default_view"),
        ]
    )

Practical guidance

Use collections for records the app needs to query later. Use settings for configuration that users or operators should be able to tweak. Use session.data for conversational scratch state that only matters inside the current session. Add typed Column metadata when the table needs richer rendering or clearer labels.

See also