Install settings
When a tenant activates your app, the platform shows a consent screen with the scopes you requested + the integrations you need. Install settings extend that screen with arbitrary inputs your app needs to function — target mailbox, digest time, language, default category set, project selection — without you having to ship a custom settings page.
The platform renders the form from your manifest, validates the
input against your schema, stores answers on the install row, and
delivers them to your handlers via ctx.config. Same “you describe,
we render” pattern as scopes and integrations.
Quickstart
Section titled “Quickstart”Declare the inputs in linkworld.app.yaml:
install_settings: - id: target_inbox type: string label: Which inbox should the app monitor? description: Email address of one of your connected M365 mailboxes. required: true
- id: digest_time type: string label: Daily digest time default: "08:00"
- id: language type: select label: Output language options: [de, en, fr] default: deRead the values at runtime:
@app.on_inboundasync def handle(ctx, env): inbox = ctx.config.get("target_inbox") if env.to != inbox: return # not the inbox the tenant configured
if ctx.config.get("language") == "de": # ...That’s it. No frontend code, no API endpoints, no validation logic on your side.
Field types
Section titled “Field types”type | Renders as | Notes |
|---|---|---|
string | One-line text input | |
text | Multi-line textarea | For long-form input like email signatures |
number | Numeric input | Optional min and max for range |
boolean | Checkbox | |
select | Dropdown | Requires options: [...] list |
select_or_custom | Dropdown with presets + free-text “Custom…” entry | Requires options: [...]; the user picks a preset OR types their own. Stored value is whatever string ends up in the field — option membership is not enforced. |
project_picker | Project dropdown | Auto-populated with the tenant’s projects + a “Create new” option |
secret | Password input | Stored in the SecretVault, stripped from ctx.config. Read via ctx.secrets.get(...) at runtime. |
Out-of-the-box activation
Section titled “Out-of-the-box activation”Two patterns make first-install of your app work without dragging the tenant through forms full of UUIDs they have to copy/paste.
select_or_custom — pick a preset or write your own
Section titled “select_or_custom — pick a preset or write your own”For free-text fields where 95% of users would pick a preset (brand
voice, signature style, output tone), use select_or_custom:
install_settings: - id: brand_voice type: select_or_custom label: Schreibweise description: Pick a preset or "Custom…" to write your own. options: - "Direct & friendly · short paragraphs · DE/EN" - "Formal (Sie) · polite · complete sentences" - "Casual (Du) · short · no signoff" default: "Direct & friendly · short paragraphs · DE/EN"The activation UI renders a dropdown with the presets + a “Custom…”
entry. Picking “Custom…” reveals a textarea where the tenant can
type their own voice. ctx.config["brand_voice"] returns whatever
string ended up in the field — option membership isn’t enforced, so
a Custom voice and a preset look the same to your handler.
auto_default — let the platform fill the field
Section titled “auto_default — let the platform fill the field”Some fields are inferable from the activation context: the activator’s
user_id, their primary email from a connected M365 mailbox. Don’t
ask the user — declare an auto_default source and the platform
fills the value server-side. The consent UI hides the field
entirely so the user never sees the UUID prompt.
install_settings: # The activator's user_id, stamped at activation. Required so the # activation fails closed if the user_id is somehow missing. - id: act_as_user_id type: string label: User for OAuth context auto_default: activator_user_id required: true
# The connected M365 mailbox's primary email, fetched from Graph # /me at activation. Falls back to empty if M365 isn't connected # (pair with required_integrations: [m365] to fail closed). - id: my_email_address type: string label: My primary email auto_default: m365_primary_emailRecognized sources:
| Source | Resolution |
|---|---|
activator_user_id | The user_id of the user clicking Activate (read from the session). |
m365_primary_email | userPrincipalName from Microsoft Graph /me. Requires the user to have completed the M365 OAuth flow — the activation layer reads linkworld_secrets via the SecretVault. Returns empty on miss. |
Override precedence: user-supplied values always win. If the
activation request body sets config[setting_id] to a non-empty
value, the auto_default resolver doesn’t run for that field. This
matters for re-activation flows where the tenant edited the value
post-install — we never overwrite their edit.
Failure handling: an auto_default source that returns empty
(Graph down, M365 not yet connected) leaves the field empty. The
field’s required: true flag is the gate — if required + empty,
activation rejects with field_errors so the consent UI can
surface the failure inline. If not required, the field stays empty
and your runtime can handle it (ctx.config.get(...) returns
None).
The project_picker — pairing with agent.vision
Section titled “The project_picker — pairing with agent.vision”The project_picker type is the bridge to the autonomous
vision-loop capability. When your manifest declares both
agent.vision and an install_settings field of type
project_picker, the platform:
- Provisions the agent (
linkworld_agentsrow) - Provisions the vision (
linkworld_agent_visionsrow scoped to the tenant’s chosen project) - The vision-loop scheduler picks up the row on its next tick and starts the ASSESS → DEBATE → PLAN → EXECUTE → REVIEW cycle
agent: name: Tax Bot Specialist system_prompt: | You handle invoice triage and bookings. vision: text: | Continuously triage tax-related emails, categorize them, and ship a weekly summary every Monday. autonomy_mode: supervised budget_max_goals: 50
install_settings: - id: project_id type: project_picker label: Where should this app's autonomous goals run? required: trueYour code doesn’t have to do anything to get the autonomous loop running — declare the vision + collect the project, and the platform wires it up.
Reading config in different runtimes
Section titled “Reading config in different runtimes”Server-side handlers
Section titled “Server-side handlers”@app.on_inboundasync def handle(ctx, env): digest_time = ctx.config.get("digest_time", "08:00")
@app.tool("compose_quote", scopes_required=["mail.send"])async def compose_quote(ctx, *, customer: str): language = ctx.config.get("language", "en") ...ctx.config is the install settings dict, populated from the
dispatch payload at request time. Empty dict if the manifest
declared no settings.
Frontend bundles (iframe)
Section titled “Frontend bundles (iframe)”By default config values are not exposed to the iframe — the bundle is untrusted UI and config can contain PII or business context partner code shouldn’t see.
To expose specific fields, mark them in the manifest:
install_settings: - id: language type: select label: Output language options: [de, en, fr] default: de expose_to_bundle: true # show in bridge.initDefault false. Never set expose_to_bundle: true for secrets,
tokens, account IDs, or anything that could leak business state to
a malicious bundle. For real secrets, use ctx.secrets.get(...)
server-side.
Validation rules
Section titled “Validation rules”The platform enforces these at activation; invalid inputs return a 400 with field-level errors so the consent UI can highlight inline:
requiredfields without a value fail with"required"string/textmust be a stringnumbermust be a number;min/maxenforcedbooleanmust be a boolean (not"true"/"false"strings)selectvalue must be inoptionsproject_pickervalue must be a UUID belonging to the tenant (cross-tenant project IDs are stripped silently)
Up to 30 settings per manifest. Tenants who want more granular config can edit individual fields post-install via the platform’s app-settings UI.
Tenant overrides post-install
Section titled “Tenant overrides post-install”Tenants change settings in the app-settings UI. Updates flow to
linkworld_tenant_apps.config and are visible on the next event
dispatch — no container restart required.
A change to the project_picker field re-runs the vision-loop
provisioning: the old vision moves to paused, a new one is
created in the new project. Vision history (debates, plans,
goals) stays attached to the old vision row for audit.
Field types
Section titled “Field types”Six field types: string, text, number, boolean, select,
select_or_custom, project_picker, secret. Server-side
ctx.config reads them in event handlers (on_inbound,
on_schedule, on_install).