All docs

Plugin API contract

The API contract between a plugin app and Cadau, for the integrating team.

Source docs/en/site/sdk-platform-api.md

Platform plugin REST API contract.

Base URL: {Cadau site}/api/v1 Version: 0.1 (aligned with Cadau backend platform_plugin_*.go)

Scope: the REST below and platform_plugins[] registration are only for apps-desktop iframe plugins. To let a work assistant call a business API from conversation (face recognition, drawing, and similar), use skills + internet capability pack. See [../../agent-capability/README.md](/docs/sdk-agent-capability) and [../README.md](/docs/sdk-platform-plugin).


0. Registration and plugin gateway

Register in Cadau mindlink.jsonplatform_plugins[] (example registration.example.json):

{
  "module_id": "my-plugin",
  "display_name": "我的插件 (My plugin)",
  "entry_base_url": "/p/my-plugin/",
  "upstream_url": "http://127.0.0.1:3010",
  "enabled": true
}
FieldMeaning
module_idUnique id; gateway path is /p/{module_id}/
entry_base_urlLaunch appends launch_token then gives it to the iframe; prefer same-origin /p/{module_id}/
upstream_urlOptional; Cadau reverse-proxies /p/{module_id}/* to this internal URL
enabledWhen false, the desktop does not show it

Gateway request path (browser → Cadau Web → backend → upstream):

GET /p/my-plugin/api/health

→ reverse-proxied to {upstream_url}/api/health (prefix /p/my-plugin is stripped).

Without upstream: only entry_base_url points at your public/standalone URL; Cadau does not proxy static pages or plugin APIs.

Implementation: backend/internal/platformplugin/proxy.go; Web Nginx location /p/ → backend.


1. Cadau side (user signed in)

1.1 List registered plugins

GET /platform-plugins
Authorization: Bearer {user access_token}

Response items[]: module_id, display_name, suite, icon, entry_base_url, enabled (internal upstream_url is not returned to the client)

1.2 Launch a plugin

POST /workspaces/{workspace_id}/platform-plugins/{module_id}/launch
Authorization: Bearer {user access_token}

Response:

FieldMeaning
module_idPlugin id
display_nameHuman-readable app name. Chinese then English in parentheses, for example “邮件 (Email)”, so people who cannot read Chinese can still recognize it
launch_tokenJWT for the plugin, about 15 minutes; the Cadau shell silently renews before expiry and pushes to the iframe
entry_urliframe entry, already with launch_token and workspace_id query params
expires_atISO 8601
api_base_urlAPI prefix relative to the site, usually /api/v1
workspace_idCurrent workspace
member_roleowner / admin / member
data_namespaceWorkspace plugin data isolation (same shape as session)

1.3 Skills shipped with the plugin (install into the workspace skill center)

An app-style plugin may ship skills/{slug}/ in the repo (see platform-plugin §7.5). Installing into the workspace skill center uses user sign-in auth, not launch_token.

Product voice: write into the skill center idempotently by slug; agents in the workspace can recall by default; do not rewrite each agent’s available-skills narrow list at seed time.

GET /workspaces/{workspace_id}/platform-plugins/{module_id}/skills
Authorization: Bearer {user access_token}

Response:

FieldMeaning
module_idPlugin id
skills[]slug, installed, up_to_date, needs_update, skill_id, name, bundle_fingerprint, installed_fingerprint
any_missingWhether any item is not installed
any_needs_updateWhether any installed item’s fingerprint is behind
can_manage_membersWhether the current user can seed

With no registered shipped skills, returns skills: [] (still 200).

POST /workspaces/{workspace_id}/platform-plugins/{module_id}/skills/seed
Authorization: Bearer {user access_token}
Content-Type: application/json

{ "slug": "practice-checklist-to-pack" }
  • Requires a member who can manage.
  • slug is optional; omit to install/upgrade all registered shipped skills for that module.
  • Response: { "ok": true, "module_id", "results": [ { slug, created, upgraded, skipped, skill_id, name, fingerprint } ] }.
  • Unknown module_id/slug (no registered pack) → 404.

Implementation: backend/internal/skillfromchat/plugin_skill_seed.go (RegisterPluginSkillBundle); compliance registration in the same package compliance_practice_seed.go; author mirror: plugins/compliance/skills/.


2. Plugin side (Bearer launch_token)

The following APIs do not need a Cadau user cookie, only:

Authorization: Bearer {launch_token}

2.1 Verify session

GET /platform-plugins/session

Response:

{
  "module_id": "my-plugin",
  "module_display_name": "我的插件 (My plugin)",
  "user_id": "uuid",
  "user_display_name": "Zhang San",
  "workspace_id": "uuid",
  "workspace_name": "R&D team",
  "member_role": "admin",
  "expires_at": "2026-07-02T10:00:00Z",
  "data_namespace": {
    "kind": "postgres_schema",
    "location": "ws_…",
    "schema": "ws_…"
  }
}

data_namespace: workspace plugin data isolation (Postgres schema or SQLite file path/DSN). Launch response includes the same field.

Agents calling the plugin: tool plugin_invoke → Cadau issues a short-lived JWT (knd=plugin_invoke) → plugin POST /api/agent/invoke. Operation list: plugin operations.json (or configure operations_url).

Error codes: plugin_launch_invalid (expired or invalid), plugin_disabled, forbidden

2.1.1 Workspace members (for binding accounts)

GET /platform-plugins/workspace-members

Bearer launch_token. Returns a short list of current workspace members (user_id, display_name, email, phone, member_role) so the plugin can bind employee files and appoint in-plugin roles. Any workspace member can read.

2.1.2 Send SMS / email on behalf

GET /platform-plugins/notify
POST /platform-plugins/notify

Bearer is launch_token, or a short-lived JWT the plugin issues with the same jwt_secret as Cadau (knd=plugin_notify, wid / mod). Used for camera callbacks and similar alerts when there is no user session.

Query channels GET: same checks as send-on-behalf (local channel or router already enabled). Do not guess in the plugin.

{ "sms": { "configured": true }, "email": { "configured": false }, "wecom": { "configured": false, "note": "WeCom messages are not enabled yet" } }

Send POST:

{ "channel": "sms", "to": "13800138000", "text": "201811" }
{ "channel": "email", "to": "ops@example.com", "subject": "Security alert", "text": "body", "html": "<p>body</p><p><img src=\"cid:snap1\" alt=\"capture\"></p>", "images": [{ "cid": "snap1", "content_type": "image/jpeg", "filename": "capture.jpg", "data_base64": "…" }] }
  • channel: sms or email (WeCom messages not enabled yet)
  • SMS uses the site SMS channel (first template item is the alert summary; prefer ≤ 60 characters)
  • Email uses the site mail channel; custom subject and body; security alerts may include html and up to two images (JPEG, cid-embedded and as attachments, so webmail can show the capture)
  • At most 40 messages per minute per workspace

Error codes: sms_not_configured, smtp_not_configured, invalid_phone, invalid_email, rate_limited

2.2 Knowledge documents

Paths are relative to the plugin theme directory plugin-{module_id}/ (Cadau maintains the index).

List

GET /platform-plugins/knowledge/items

Read

GET /platform-plugins/knowledge/content?path=guide.md

Write (member_role owner/admin)

PUT /platform-plugins/knowledge/content
Content-Type: application/json

{
  "path": "guide.md",
  "content": "# Title\n\nBody…",
  "title": "Getting started",
  "summary": "One-line summary for retrieval matching",
  "tags": ["tag1", "tag2"]
}

Delete (owner/admin)

DELETE /platform-plugins/knowledge/content?path=guide.md

Batch sync (owner/admin)

POST /platform-plugins/knowledge/sync
Content-Type: application/json

{
  "documents": [
    { "path": "a.md", "content": "…", "title": "A" },
    { "path": "b.md", "content": "…", "title": "B" }
  ]
}

Response: { "ok": true, "synced": ["a.md","b.md"], "count": 2, "theme_dir": "plugin-my-plugin" }

Scope: plugin knowledge sync is Markdown body (path + content). Images/short video and other rich media in workspace knowledge are managed on the Cadau product side and are not uploaded via this sync API.

2.2.1 Standards library read-only (workspace regulations and management standards)

Source of truth and publish flow: product §3.2.2 / docs/core-mechanisms/规范库.md. The plugin may read only published and in-effect entries.

GET /platform-plugins/standards
GET /platform-plugins/standards?tag=hr
GET /platform-plugins/standards/{standardId}
GET /platform-plugins/standards/{standardId}/content

Auth: Authorization: Bearer <launch_token> (same as knowledge APIs).

2.2.2 Asset library read-only (workspace templates and tables)

Source of truth and publish flow: product §3.2.3 / docs/core-mechanisms/资料库.md. The plugin may read only published entries; /file returns a binary file stream.

GET /platform-plugins/assets
GET /platform-plugins/assets?tag=hr
GET /platform-plugins/assets/{assetId}
GET /platform-plugins/assets/{assetId}/file

Auth: Authorization: Bearer <launch_token> (same as knowledge APIs).

2.2.3 Workspace model (structured JSON)

Plugins must not bring their own model API key. For short-lived structured results such as “generate a survey from this body / recommend staffing”, use this API with the large model already configured for this workspace.

Unlike “in-plugin embed assistant conversation” (§2.3 embed): this API is a one-shot server system+user → JSON text, with no conversation state.

POST /platform-plugins/ai/json
Authorization: Bearer <launch_token>
Content-Type: application/json

{
  "system": "You are……output one JSON object only: {...}",
  "user": "{ \"context\": \"…\" }"
}

Response:

{ "text": "{ \"…\": \"JSON string the model returned (may still include prose; the caller should parse/clean)\" }" }
ConstraintMeaning
Authlaunch_token (same as knowledge / standards library)
system / userBoth required; length counted in Unicode characters: system ≤ 32000, user ≤ 400000 (fits large-context models; business should still split by task)
Model not configuredHTTP 503, codellm_disabled, copy says the workspace has no large model yet
Call failedHTTP 502, codellm_error
SDKGo Client.AIJSON · JS client.aiJSON(system, user)
Optionalattachment_ids attach images/PDF; pdf_pages=true attaches page screenshots even when the PDF has a text layer (used to extract ID photos)

Official usage: plugins/hr staffing recommendations, resume photo and certificate extract, plugins/compliance generate surveys from standards.

2.2.4 Plugin upload extract / crop

Resume analysis and similar extract embedded images from an already uploaded PDF / Word (.docx) / web resume / image, or crop ID photos and certificate scans by a normalized box. Results are written to the current user’s upload library and return a new file_id.

POST /platform-plugins/uploads/{id}/extract-images
Authorization: Bearer <launch_token>
POST /platform-plugins/uploads/{id}/crop
Authorization: Bearer <launch_token>
Content-Type: application/json

{ "items": [{ "page": 1, "x": 0.7, "y": 0.05, "width": 0.25, "height": 0.28, "label": "Applicant photo" }] }

x/y/width/height are 0–1 relative to the page (or the whole image). Both responses are { "images": [{ "file_id", "filename", "mime_type", "width", "height", "page" }] }.

2.3 In-plugin assistant token

POST /platform-plugins/embed-token
Content-Type: application/json

{
  "user_agent_id": "uuid",
  "app_id": "my-plugin",
  "ttl_seconds": 3600
}

Response is similar to POST /user-agents/{id}/embed-token: access_token, token_type, expires_in, expires_at, workspace_id, user_agent_id, app_id, permanent, record_id

Then the frontend loads {Cadau}/embed/mindlink-widget.min.js and init (see the embed SDK contract).

2.4 Start a workflow (approvals must include a document snapshot)

When a business document goes through workflow approval, write the full text to review into variables.approval_doc at start. The engine lifts it onto the instance payload and the approval step renders it. The engine does not fetch the document from the plugin later. Do not send only an id / title and send the approver back to the app.

GET /platform-plugins/workflows/definitions
POST /platform-plugins/workflows/start
Authorization: Bearer <launch_token>
Content-Type: application/json
{
  "definition_id": "uuid",
  "title": "Submit staffing request · YR-2026-0001",
  "variables": {
    "staffing_req_id": "uuid",
    "approval_doc": {
      "title": "Staffing request YR-2026-0001",
      "subtitle": "Draft from headcount gap",
      "fields": [
        { "label": "Applicant", "value": "Chen Chen" },
        { "label": "Department", "value": "Corporate support" }
      ],
      "tables": [{
        "title": "Lines",
        "columns": ["Department", "Position", "Headcount"],
        "rows": [["Corporate support", "Finance specialist", "1"]]
      }],
      "note": ""
    }
  }
}
FieldMeaning
definition_idWorkspace workflow definition id (GET …/definitions)
titleInstance title, for people
variables.approval_docRequired for approval-style flows. Object or JSON string. Shape: ApprovalDoc below
Other variablesFlat keys for write-back (e.g. staffing_req_id), separate from the snapshot

ApprovalDoc:

FieldTypeMeaning
titlestringDocument title (include the number)
subtitlestringOptional subtitle
fields[]{ label, value }Header: applicant, department, reason, validity, not just ids
tables[]{ title?, columns[], rows[][] }Line items
notestringOptional note

SDK: Go Client.StartWorkflow + WithApprovalDoc / FieldListApprovalDoc; TS client.startWorkflow + withApprovalDoc / fieldListApprovalDoc. Type ApprovalDoc.

Auto-node invoke success may also return top-level approval_doc (for example a signing envelope created mid-flow). Submit-then-approve still writes the snapshot at start.

See plugins/hr staffing submit and hire/leave/transfer; product source of truth workflow.json-v1.md “approval document snapshot”.


3. Plugin-owned HTTP conventions (recommended)

PathMeaning
POST /api/bootstrapbody { "launch_token" } → verify session, optionally sync knowledge, return user context (including data_namespace)
GET /api/sessionRefresh session with the launch token in Bearer or cookie
GET /api/healthHealth check
GET /operations.jsonOperations an agent can call; if there is a fill dialog, must include fill_open_form (see §3.2); may declare workflow_steps in the same file; or set operations_url at registration
POST /api/agent/invoke(Optional) agent plugin_invoke and workflow auto-nodes; verify JWT (knd=plugin_invoke), not launch_token

iframe session: frontend stores launch_token in sessionStorage; later requests send Authorization: Bearer …. Plugin-owned API paths should use a relative path or resolvePluginAPI(), so they work with the Cadau same-origin gateway /p/{module_id}/.

3.1 Host ↔ plugin postMessage

DirectiontypeMeaning
Host → pluginmindlink:launch_tokenSilent renew; must write sessionStorage (JS: listenHostLaunchToken)
Host → pluginmindlink:themetheme: light \dark (JS: listenHostTheme)
Host → pluginmindlink:localelocale: zh \en (JS: listenHostLocale)
Host → pluginmindlink:plugin_tabHelp deep-links and similar switch in-plugin tab (JS: listenHostPluginTab)
Host → pluginmindlink:plugin_data_changedBusiness data changed; suggest refresh (JS: listenHostPluginDataChanged)
Host → pluginmindlink:plugin_form_fillWrite fields the assistant parsed back into the current draft (§3.2; JS: listenHostPluginFormFill)
Plugin → hostmindlink:request_launch_tokensource must be mindlink-plugin; ask for a token on hot reload/401 (JS: requestHostLaunchToken)
Plugin → hostmindlink:request_localesource must be mindlink-plugin; ask for current language on first screen or hot reload (JS: requestHostLocale)
Plugin → hostmindlink:plugin_ui_changedReport the currently open fill dialog (§3.2; JS: postPluginUiChanged)

Host messages have source mindlink; plugin messages have source mindlink-plugin.

3.2 Recognize the open form (host protocol)

Plugins with a fillable dialog must implement this. Product voice: [../README.md §6.1](/docs/sdk-platform-plugin); spec §4.1.5.

When the user opens a fill dialog, the app assistant must recognize this form. Unless they name another record, natural-language operations default to the current screen; they do not need to name the form. Write natural language back into the draft and show it immediately; until the user says “save / create / submit / complete”, do not write the DB.

#### 3.2.1 Plugin → host: mindlink:plugin_ui_changed

window.parent.postMessage, source must be mindlink-plugin. JS: postPluginUiChanged.

{
  "source": "mindlink-plugin",
  "type": "mindlink:plugin_ui_changed",
  "module_id": "my-plugin",
  "tab": "home",
  "dialog": "draft",
  "dialog_title": "Example draft",
  "form": { "name": "Zhang San" },
  "field_hints": "Name,notes",
  "readonly": false
}
FieldMeaning
module_idSame as registration
tabCurrent tab (optional)
dialogCurrent dialog id; on close pass an empty string, and the host clears “the form being filled”
dialog_titleUser-visible title (used when asked “what is on screen”)
formAlready-filled non-empty fields (strings); an empty draft may report only dialog + field_hints
field_hintsUser-language names of writable fields, comma-separated, for example Name,notes
readonlytrue: view dialog; the assistant only names the title and does not call fill_open_form

Cadau puts dialog / dialog_title / form / field_hints / readonly into conversation client_context.

#### 3.2.2 Operation fill_open_form

In operations.json, name must be fill_open_form, mutates: false. Cadau POST /api/agent/invoke body:

{
  "operation": "fill_open_form",
  "args": {
    "form": "draft",
    "fields": { "name": "Zhang San", "备注": "urgent" }
  }
}
argsMeaning
formSame as current dialog; may be omitted (host fills the open dialog). May also accept dialog / case_type
fieldsKey-values to write; Chinese or English field names; may nest; host/SDK flatten

Plugin success response (may be wrapped in { "ok": true, "result": { … } }; the host looks down for ui_action):

{
  "ui_action": "fill_form",
  "form": "draft",
  "fields": { "name": "Zhang San", "note": "urgent" },
  "unresolved": [],
  "note": "Written into the currently open form; not saved to the system yet"
}
FieldMeaning
ui_actionMust be fill_form, or the host will not write back to the UI
formTarget draft id
fieldsNormalized strings that can go straight into inputs (names already resolved to ids belong here)
unresolvedOptional; { "field", "value", "reason" }[], tell the assistant when it does not match an existing record
noteOptional; one sentence the assistant can read

Do not INSERT/UPDATE business tables in this operation. When you need to resolve “R&D” → department id, only change fields; still do not write the DB.

Go: FillOpenFormPassthrough(body.Args); TS: fillOpenFormPassthrough(args). Skeleton: templates/starter/cmd/plugin/main.go.

#### 3.2.3 Host → plugin: mindlink:plugin_form_fill

After the Cadau shell parses ui_action=fill_form, it postMessages the iframe (source=mindlink). JS: listenHostPluginFormFill.

{
  "source": "mindlink",
  "type": "mindlink:plugin_form_fill",
  "module_id": "my-plugin",
  "form": "draft",
  "fields": { "name": "Zhang San", "note": "urgent" }
}

The plugin merges fields into the currently open draft whose dialog matches, and refreshes inputs immediately. mutates: false; the host does not refresh lists because of this.

3.3 Extra workflow steps and list contract (workflow_steps / rubric)

A plugin can declare workflow_steps in the same operations.json, hanging domain auto-nodes on the process designer (they appear in the catalog only when that plugin is enabled). When the engine reaches that step it calls POST /api/agent/invoke (same entry as plugin_invoke).

{
  "module_id": "my-plugin",
  "operations": [
    { "name": "workflow_load_checklist", "mutates": false, "min_role": "member" }
  ],
  "workflow_steps": [
    {
      "type": "my-plugin.load_checklist",
      "label": "Load material list",
      "description": "Write instance rubric for upload evidence / auto score",
      "color": "#0891b2",
      "kind": "auto",
      "operation": "workflow_load_checklist",
      "config_hint": "Optional phases"
    }
  ]
}
workflow_steps[] fieldMeaning
typeStep type (prefer {module_id}.{action})
kindCurrently only auto: on arrival Invoke, then advance on always after success
operationMatching operation name in operations
label / description / colorDesigner display

Success response (load-style nodes return rubric; write-back nodes may return only variables):

{ "ok": true, "rubric": [ /* RubricItem[] */ ], "approval_doc": { }, "variables": { } }
  • rubric: written onto the instance for core nodes upload evidence (human.upload) and auto score (llm.score); must match RubricItem below. Required for load-style nodes.
  • approval_doc: optional. Business-document snapshot shown on the approval step (same idea as check items). Submit-then-approve should write variables.approval_doc at start (see §2.4), not only patch it from an auto-node. Shape: §2.4 ApprovalDoc.
  • variables: merged into instance variables (flat keys). Write-back nodes (confirm staffing request, and similar) use this for staffing_req_status etc.
  • Failure: { "ok": false, "error": "user-visible explanation", "rubric": [] }.
  • Put ok at the HTTP body top level (do not nest it only under result). See plugins/compliance load standards and plugins/hr confirm/return staffing request.

#### RubricItem (list item · implementation source of truth)

Aligned with backend backend/internal/standards.RubricItem and SDK type RubricItem (Go / TS). Other domain load nodes (onboarding materials, training confirmation, and similar) must output the same shape; do not invent field names.

FieldTypeRequiredMeaning
idstringYesStable primary key; upload item_photos, comments, and score results all attach by this id
requirement_textstringYesRequirement copy shown on the handling page
scoring_textstringUpload can be loose; recommended when connecting auto scorePass/score criteria
codestringNoDisplay number
phasestringNoPhase key (compared lowercase); for upload-node config.phases filter
tierstringNored \baseline \excellence
photo_standard_textstringNoPhoto/evidence hint
reference_photo_upload_idsstring[]NoPassing sample image upload_ids (at most 3); handling page shows them for comparison
must_passboolNoRed-line item; default false
iway_refstringNoDomain reference; omit if not generally needed

Reference: plugins/compliance workflow_load_check_itemstoRubric. Product notes: docs/core-mechanisms/workflow.json-v1.md §check-item list contract; mechanism guide: [../README.md](/docs/sdk-platform-plugin) §6.2.

3.4 Cookie / storage names (do not mix)

NameWho writesUse
ml_plugin_gwCadau gatewayPath=/p/{module_id}/; gateway auth when the browser hits the reverse-proxy path
ml_plugin_launchPlugin process (Go bootstrap)Helper cookie on the plugin origin (optional)
ml_plugin_launch_tokenPlugin frontend sessionStorageBearer source of truth; must update after renew

4. Security response headers (plugin service)

Content-Security-Policy: frame-ancestors 'self' http://localhost:8080 http://127.0.0.1:8080

In production, replace with the customer Cadau Web origin. Go SDK: mindlinkplugin.FrameAncestorsCSP(...).


5. Error body

{
  "error": "user-visible explanation",
  "code": "machine_code",
  "request_id": "…"
}

Common code: unauthorized, plugin_launch_invalid, forbidden, validation_error, not_found