Skip to content

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.

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: de

Read the values at runtime:

@app.on_inbound
async 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.

typeRenders asNotes
stringOne-line text input
textMulti-line textareaFor long-form input like email signatures
numberNumeric inputOptional min and max for range
booleanCheckbox
selectDropdownRequires options: [...] list
select_or_customDropdown with presets + free-text “Custom…” entryRequires 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_pickerProject dropdownAuto-populated with the tenant’s projects + a “Create new” option
secretPassword inputStored in the SecretVault, stripped from ctx.config. Read via ctx.secrets.get(...) at runtime.

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_email

Recognized sources:

SourceResolution
activator_user_idThe user_id of the user clicking Activate (read from the session).
m365_primary_emailuserPrincipalName 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:

  1. Provisions the agent (linkworld_agents row)
  2. Provisions the vision (linkworld_agent_visions row scoped to the tenant’s chosen project)
  3. 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: true

Your 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.

@app.on_inbound
async 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.

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.init

Default 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.

The platform enforces these at activation; invalid inputs return a 400 with field-level errors so the consent UI can highlight inline:

  • required fields without a value fail with "required"
  • string / text must be a string
  • number must be a number; min / max enforced
  • boolean must be a boolean (not "true" / "false" strings)
  • select value must be in options
  • project_picker value 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.

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.

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).