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.json → platform_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
}
| Field | Meaning |
|---|---|
module_id | Unique id; gateway path is /p/{module_id}/ |
entry_base_url | Launch appends launch_token then gives it to the iframe; prefer same-origin /p/{module_id}/ |
upstream_url | Optional; Cadau reverse-proxies /p/{module_id}/* to this internal URL |
enabled | When 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:
| Field | Meaning |
|---|---|
module_id | Plugin id |
display_name | Human-readable app name. Chinese then English in parentheses, for example “邮件 (Email)”, so people who cannot read Chinese can still recognize it |
launch_token | JWT for the plugin, about 15 minutes; the Cadau shell silently renews before expiry and pushes to the iframe |
entry_url | iframe entry, already with launch_token and workspace_id query params |
expires_at | ISO 8601 |
api_base_url | API prefix relative to the site, usually /api/v1 |
workspace_id | Current workspace |
member_role | owner / admin / member |
data_namespace | Workspace 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:
| Field | Meaning |
|---|---|
module_id | Plugin id |
skills[] | slug, installed, up_to_date, needs_update, skill_id, name, bundle_fingerprint, installed_fingerprint |
any_missing | Whether any item is not installed |
any_needs_update | Whether any installed item’s fingerprint is behind |
can_manage_members | Whether 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.
slugis 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:smsoremail(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
htmland up to twoimages(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;
/filereturns 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)\" }" }
| Constraint | Meaning |
|---|---|
| Auth | launch_token (same as knowledge / standards library) |
system / user | Both required; length counted in Unicode characters: system ≤ 32000, user ≤ 400000 (fits large-context models; business should still split by task) |
| Model not configured | HTTP 503, code ≈ llm_disabled, copy says the workspace has no large model yet |
| Call failed | HTTP 502, code ≈ llm_error |
| SDK | Go Client.AIJSON · JS client.aiJSON(system, user) |
| Optional | attachment_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": ""
}
}
}
| Field | Meaning |
|---|---|
definition_id | Workspace workflow definition id (GET …/definitions) |
title | Instance title, for people |
variables.approval_doc | Required for approval-style flows. Object or JSON string. Shape: ApprovalDoc below |
Other variables | Flat keys for write-back (e.g. staffing_req_id), separate from the snapshot |
ApprovalDoc:
| Field | Type | Meaning |
|---|---|---|
title | string | Document title (include the number) |
subtitle | string | Optional subtitle |
fields[] | { label, value } | Header: applicant, department, reason, validity, not just ids |
tables[] | { title?, columns[], rows[][] } | Line items |
note | string | Optional 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)
| Path | Meaning |
|---|---|
POST /api/bootstrap | body { "launch_token" } → verify session, optionally sync knowledge, return user context (including data_namespace) |
GET /api/session | Refresh session with the launch token in Bearer or cookie |
GET /api/health | Health check |
GET /operations.json | Operations 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
| Direction | type | Meaning | |
|---|---|---|---|
| Host → plugin | mindlink:launch_token | Silent renew; must write sessionStorage (JS: listenHostLaunchToken) | |
| Host → plugin | mindlink:theme | theme: light \ | dark (JS: listenHostTheme) |
| Host → plugin | mindlink:locale | locale: zh \ | en (JS: listenHostLocale) |
| Host → plugin | mindlink:plugin_tab | Help deep-links and similar switch in-plugin tab (JS: listenHostPluginTab) | |
| Host → plugin | mindlink:plugin_data_changed | Business data changed; suggest refresh (JS: listenHostPluginDataChanged) | |
| Host → plugin | mindlink:plugin_form_fill | Write fields the assistant parsed back into the current draft (§3.2; JS: listenHostPluginFormFill) | |
| Plugin → host | mindlink:request_launch_token | source must be mindlink-plugin; ask for a token on hot reload/401 (JS: requestHostLaunchToken) | |
| Plugin → host | mindlink:request_locale | source must be mindlink-plugin; ask for current language on first screen or hot reload (JS: requestHostLocale) | |
| Plugin → host | mindlink:plugin_ui_changed | Report 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
}
| Field | Meaning |
|---|---|
module_id | Same as registration |
tab | Current tab (optional) |
dialog | Current dialog id; on close pass an empty string, and the host clears “the form being filled” |
dialog_title | User-visible title (used when asked “what is on screen”) |
form | Already-filled non-empty fields (strings); an empty draft may report only dialog + field_hints |
field_hints | User-language names of writable fields, comma-separated, for example Name,notes |
readonly | true: 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" }
}
}
args | Meaning |
|---|---|
form | Same as current dialog; may be omitted (host fills the open dialog). May also accept dialog / case_type |
fields | Key-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"
}
| Field | Meaning |
|---|---|
ui_action | Must be fill_form, or the host will not write back to the UI |
form | Target draft id |
fields | Normalized strings that can go straight into inputs (names already resolved to ids belong here) |
unresolved | Optional; { "field", "value", "reason" }[], tell the assistant when it does not match an existing record |
note | Optional; 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[] field | Meaning |
|---|---|
type | Step type (prefer {module_id}.{action}) |
kind | Currently only auto: on arrival Invoke, then advance on always after success |
operation | Matching operation name in operations |
label / description / color | Designer 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 matchRubricItembelow. 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 writevariables.approval_docat start (see §2.4), not only patch it from an auto-node. Shape: §2.4ApprovalDoc.variables: merged into instance variables (flat keys). Write-back nodes (confirm staffing request, and similar) use this forstaffing_req_statusetc.- Failure:
{ "ok": false, "error": "user-visible explanation", "rubric": [] }. - Put
okat the HTTP body top level (do not nest it only underresult). Seeplugins/complianceload standards andplugins/hrconfirm/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.
| Field | Type | Required | Meaning | ||
|---|---|---|---|---|---|
id | string | Yes | Stable primary key; upload item_photos, comments, and score results all attach by this id | ||
requirement_text | string | Yes | Requirement copy shown on the handling page | ||
scoring_text | string | Upload can be loose; recommended when connecting auto score | Pass/score criteria | ||
code | string | No | Display number | ||
phase | string | No | Phase key (compared lowercase); for upload-node config.phases filter | ||
tier | string | No | red \ | baseline \ | excellence |
photo_standard_text | string | No | Photo/evidence hint | ||
reference_photo_upload_ids | string[] | No | Passing sample image upload_ids (at most 3); handling page shows them for comparison | ||
must_pass | bool | No | Red-line item; default false | ||
iway_ref | string | No | Domain reference; omit if not generally needed |
Reference: plugins/compliance workflow_load_check_items → toRubric. 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)
| Name | Who writes | Use |
|---|---|---|
ml_plugin_gw | Cadau gateway | Path=/p/{module_id}/; gateway auth when the browser hits the reverse-proxy path |
ml_plugin_launch | Plugin process (Go bootstrap) | Helper cookie on the plugin origin (optional) |
ml_plugin_launch_token | Plugin frontend sessionStorage | Bearer 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