All docs

App UI description

Workspace apps · UI description spec (full).

Source docs/en/site/sdk-appsdk-ui.md

Workspace apps · UI description spec (full).

Doc version: 1.0 (matches app.json / AppSpec v1) Readers: people who hand-write or review an app UI description (including the app-development assistant when it revises a draft) Related: appsdk directory notes · App views · Hand-build an app · Workspace apps mechanism


1. Core principles

  1. The UI is generated from a description, not a hand-written web page. The platform Web standard renderer reads app.json and draws lists and forms from views.
  2. One description does three things: what data looks like (entities) → which blocks the page has (views) → what buttons call (actions).
  3. User-visible copy uses label; name / id / action are program ids (lowercase, underscores, and similar). Do not use them as UI titles.
  4. Currently only three views: list | form | action_form. There is no standalone layout model; extensions and “UI from a picture” are in App views.

When the app opens, the renderer draws each block top to bottom in views array order.


2. Files and pack location

FileRole
app.jsonUI and data-model description (this spec’s main subject)
manifest.jsonApp manifest: display name, assembly, ui_mode=metadata, and similar (usually written by the platform)
data/app.dbBusiness DB generated from entities (separate from the Cadau main DB)
logic/handlers.pyCustom action implementations with impl=script (called after the UI submits)
schema/001_init.sqlCREATE TABLE SQL generated from entities (maintained when the spec is created/updated)

When hand-writing the UI, you usually only change app.json. If you add a custom action, you also implement the matching action id in handlers.py.


3. Root object

{
  "version": 1,
  "entities": [ /* at least one */ ],
  "views": [ /* at least one */ ],
  "actions": [ /* actions referenced by action_form must be declared here */ ]
}
FieldRequiredMeaning
versionRecommendedCurrently 1; if omitted the platform fills 1 on write
entitiesYesBusiness-object (table) definitions; at least 1
viewsYesUI blocks; at least 1
actionsConditionalIf there is an action_form, you must declare the action it references

Validation failure cannot save/publish (see §10).


4. Entities entities[]

One business table (SQLite) inside the app. The platform automatically adds primary-key column id (autoincrement). You need not declare id in fields.

{
  "name": "profile_reports",
  "label": "Profile records",
  "fields": [
    { "name": "emp_name", "label": "Employee name", "type": "text", "required": true },
    { "name": "status", "label": "Report status", "type": "select", "default": "待生成",
      "options": ["待生成", "已生成", "失败"] },
    { "name": "created_at", "label": "Generated at", "type": "datetime", "read_only": true, "auto": "now" }
  ]
}

4.1 Entity attributes

FieldRequiredRules
nameYesTable name; ^[a-z][a-z0-9_]*$ (starts with a lowercase letter; lowercase, digits, underscore only)
labelYesUser-visible name (for example “Profile records”)
fieldsYesAt least 1 field (excluding system id)

4.2 Fields fields[]

FieldRequiredMeaning
nameYesColumn name; same style as entity names; do not use id (system-reserved)
labelYesColumn header / form label
typeRecommendedSee the table below; empty string is treated as text
requiredNotrue → column NOT NULL; form shows *
defaultNoDefault (written into DDL; form initial value depends on the control)
minNonumber only: HTML min
read_onlyNotrue: not shown on editable forms
autoNoCurrently "now": on create the frontend writes UTC ISO (no input shown)
hiddenNotrue: by default not shown as a list column or on the form
optionsNoString list of options for select

Field types and UI controls:

typeControlSQLite column type
text (default)Single-line inputTEXT
numbertype=numberINTEGER
textareaMulti-line textTEXT
datetimedatetime-local (wall clock in the account display timezone)TEXT (stored as UTC ISO)
selectDropdown; must have optionsTEXT

Time conventions (aligned with product “time display and timezone preference”):

  • On disk: datetime / auto=now are UTC absolute-time strings; do not store “Beijing time pretending to be local, with no timezone” in the DB.
  • List display: the platform formats by the current user’s display timezone, date format, and 12/24-hour setting; not the browser system timezone.
  • Form input: the user fills wall clock in the display timezone; submit converts to UTC ISO.
  • Account preference itself cannot be written into app.json; point users to “Me → Preferences → Time display”.

4.3 Special list convention

If the entity has a field named upload_id (stores an attachment id), the list Actions column offers Open / Download for rows that have a value (used for employee-profile HTML reports and similar). This is a renderer convention, not a separate view type.


5. Views views[] (how the UI is drawn)

Each element is one UI block. type decides what is drawn.

5.1 List list

Shows a table for an entity; data comes from built-in action crud.list.

{
  "type": "list",
  "id": "history",
  "label": "Past profiles",
  "entity": "profile_reports",
  "columns": ["emp_name", "report_title", "status", "created_at"],
  "limit": 100,
  "allow_delete": true
}
FieldRequiredMeaning
typeYes"list"
entityYesMust be a declared entity name
labelRecommendedBlock title; default “List”
idNoFrontend key; recommended for stability
columnsNoColumn order (field names); if omitted, all non-hidden, non-id entity fields
limitNoRows to fetch; default 100
allow_deleteNoOmitted or true: show delete; false: hide delete. Before delete, a product-style confirm (not the browser native one)

UI behavior: “Refresh” next to the title; delete calls crud.delete.

5.2 Entity form form

Inserts one row into an entity; submit calls crud.create.

{
  "type": "form",
  "label": "New note",
  "entity": "notes",
  "mode": "create",
  "fields": ["title", "body"]
}
FieldRequiredMeaning
typeYes"form"
entityYesTarget entity
modeRecommendedCurrent implementation treats as create; write "create"
fieldsNoSubset of field names to show; if omitted, all editable entity fields
labelRecommendedBlock title; default “New”

Fields that do not appear on the form: id, hidden=true, read_only=true, auto=now (auto=now is written by the frontend on submit; no input shown).

5.3 Action form action_form

Collects params and calls a custom action (usually impl=script).

{
  "type": "action_form",
  "id": "generate",
  "label": "Generate employee profile",
  "action": "profile.generate"
}
FieldRequiredMeaning
typeYes"action_form"
actionYesMust match actions[].id
labelRecommendedBlock title
idNoRecommended
fieldsDoes not drive controlsSee “Important” below

Important (not intuitive): when the standard renderer draws an action form, inputs come from actions[].params, not views[].fields. views[].fields may be documentation/assistant hints, but the current frontend does not generate controls from it. When hand-writing, put form fields on the matching action’s params.

After submit: calls invoke(action, params). If the result includes upload_id (and optional filename), the UI can offer open/download report (same as employee profile).


6. Actions actions[]

{
  "id": "profile.generate",
  "label": "Generate employee profile",
  "impl": "script",
  "params": [
    { "name": "emp_name", "label": "Employee name", "type": "text", "required": true },
    { "name": "emp_id", "label": "Employee ID (when names collide)", "type": "text" }
  ]
}
FieldRequiredMeaning
idYesAction id; prefer domain.verb (for example profile.generate)
labelYesUser-visible name
implYes"script": goes through logic/handlers.py; "crud.create": built-in create (rare; usually a form view is enough)
paramsRecommendedField definitions for the action form; decide which inputs appear on action_form

6.1 Params params[]

FieldMeaning
nameParam name submitted to the script
labelForm label
typeSame as field types: text / number / …; default text
requiredWhether required

select on action params: if you need a dropdown, the current action-param structure does not include options. Put dropdown options on entity fields and use form/list. Action forms currently center on text/number/textarea/datetime (aligned with renderer resolveActionFields). For enums, use text and validate in the script, or extend the contract later.

6.2 Built-in actions (need not be in actions)

Provided by the platform; lists/forms call them automatically:

actionUse
crud.listList load
crud.createEntity form submit
crud.update(API exists; standard UI has no edit view yet)
crud.deleteList delete
schema.tablesList business tables
pingHealth check

A custom action referenced by action_form but missing from actionsvalidation fails.

6.3 Mapping to handlers.py

When impl=script, runtime calls the in-pack script entry, roughly:

handle(action, params, ctx) -> dict
  • action equals actions[].id (for example profile.generate)
  • params are key-values submitted from the form
  • Return should include ok; on failure include error; optional upload_id / warnings and similar for the UI

Script detail and the platform bridge (fetch data, save attachments) are in the mechanism docs under “invoke”. Whether a button appears on the UI is still decided by this file’s actions + action_form.


7. Minimal hand-written example (notebook)

{
  "version": 1,
  "entities": [
    {
      "name": "notes",
      "label": "Notes",
      "fields": [
        { "name": "title", "label": "Title", "type": "text", "required": true },
        { "name": "body", "label": "Body", "type": "textarea" },
        { "name": "created_at", "label": "Created at", "type": "datetime", "read_only": true, "auto": "now" }
      ]
    }
  ],
  "views": [
    {
      "type": "form",
      "label": "New note",
      "entity": "notes",
      "mode": "create",
      "fields": ["title", "body"]
    },
    {
      "type": "list",
      "label": "All notes",
      "entity": "notes",
      "columns": ["title", "created_at"],
      "allow_delete": true
    }
  ]
}

No actions needed: only form + list, all built-in CRUD.


8. Hand-written example with a custom action (sketch)

{
  "version": 1,
  "entities": [
    {
      "name": "jobs",
      "label": "Job records",
      "fields": [
        { "name": "keyword", "label": "Keyword", "type": "text", "required": true },
        { "name": "status", "label": "Status", "type": "select", "options": ["待处理", "完成", "失败"] },
        { "name": "upload_id", "label": "Attachment ID", "type": "text", "hidden": true },
        { "name": "created_at", "label": "Time", "type": "datetime", "read_only": true, "auto": "now" }
      ]
    }
  ],
  "views": [
    {
      "type": "action_form",
      "id": "run",
      "label": "Run job",
      "action": "job.run"
    },
    {
      "type": "list",
      "label": "History",
      "entity": "jobs",
      "columns": ["keyword", "status", "created_at"]
    }
  ],
  "actions": [
    {
      "id": "job.run",
      "label": "Run job",
      "impl": "script",
      "params": [
        { "name": "keyword", "label": "Keyword", "type": "text", "required": true }
      ]
    }
  ]
}

You also need to implement job.run in logic/handlers.py (produce a result, optionally write the jobs table). Changing only the UI description without a script will fail on submit.


9. Employee-profile walkthrough (live structure)

What the user seesWhat it maps to in the description
“Generate employee profile” formviews type=action_form, action=profile.generate; fields from that action’s params
“Past profiles” tableviews type=list, entity=profile_reports
Delete historyallow_delete not set to false
Open/download reportEntity has upload_id, and the row has a value
Fetch and HTML after submitNot part of the UI description; in logic/ + references/pipeline.json and similar

A complete sample is the app.json in the runtime app pack (employee-profile template).


10. Validation checklist (self-check after hand-writing)

  • [ ] entities ≥ 1, and each has name/label/fields
  • [ ] Entity names and field names match the lowercase-id rules; fields have label; types are valid
  • [ ] views ≥ 1; type is only list | form | action_form
  • [ ] list/form entity exists
  • [ ] Each action_form action exists in actions
  • [ ] Each actions[] has id/label; impl is script or crud.create
  • [ ] Action-form fields are on actions[].params, not only on views[].fields
  • [ ] Custom actions are implemented in handlers.py (if impl=script)
  • [ ] User-visible copy is all in label; English ids are not used as titles

Implementation validator: workspaceapp.ValidateSpec.


11. Explicitly out of this spec

NeedGo to
Free layout, own CSS/routes, full webPlatform plugin SDK · App views
Fetch steps, HTML templates, assemble scriptsApp data pipeline, App actions
“Make an app” from conversationWorkspace apps §create from conversation
Product help (end users)help/product-features/workspace-apps.md
Hand-build a pack from scratchHand-build an app · App package layout

12. Revision convention

  • Contract changes (new view types, field attributes, action-param options, and similar) must sync: this file, spec.go validation, WorkspaceAppRenderer.tsx render behavior, and bump this section’s doc version.
  • Mechanism overview Workspace apps may keep a short summary; full hand-write notes are this document.