Skip to content

Latest commit

 

History

History
207 lines (161 loc) · 12.4 KB

plugin-endpoints.mdx

File metadata and controls

207 lines (161 loc) · 12.4 KB
title Plugin endpoints — add routes from a plugin
navTitle Plugin Endpoints
description REST endpoints each installed plugin adds — auth, workflow, automation, views, realtime, notifications, AI, i18n and files — and how to discover them.

These REST endpoints are only available when the corresponding plugin is installed. Check the discovery manifest services map before calling them; all paths are relative to the base URL (defaults to /api/v1).

This page is a catalog of routes the platform serves for you — it is not about authoring an endpoint of your own. To publish your own URL as metadata, see Declarative Endpoints.

Authentication (Plugin Required)

These endpoints are only available when an auth plugin is installed. Check `discovery.services.auth.enabled` first.

POST /auth/sign-in/email

Authenticate with email and password (better-auth's email sign-in route, mounted under the auth plugin's /auth prefix) and receive a session token. There is no /auth/login route.

Body: { email: "admin@example.com", password: "secret" }
Response: { token: "jwt...", user: { ... } }


Plugin-Provided Service Endpoints

The following endpoints become available when the corresponding plugin is installed and registered with the kernel. Use the discovery services map to check availability.

Workflow (/workflow) — removed in v17

There is no workflow endpoint, and there is no `workflow` service slot. The three routes documented here were declared in the API protocol and served by nothing — no dispatcher handler, no plugin — so they 404'd for the whole life of the declaration. The slot, the `WorkflowProtocol` methods behind it and the discovery fields that reported it were all retired in v17 ([#4451](#4451)). Use the live mechanisms instead: an object validation rule of type `state_machine` for lifecycle transitions, an `approval` flow node for human approval pauses (ADR-0019), and lifecycle hooks / `record_change` flows for record-triggered automation.

Approve/reject were never workflow routes (ADR-0019): approval is a flow node, and decisions are recorded on the approvals runtime via POST /approvals/requests/:id/approve and POST /approvals/requests/:id/reject.

Automation (/automation) — Plugin Required

Method Endpoint Description
POST /automation/:name/trigger Trigger an automation flow by name (legacy alias: /automation/trigger/:name). Failures answer real status codes, not a 200 wrapping an inner failure: 404 unknown flow, 409 FLOW_DISABLED, 422 FLOW_NO_START_NODE, 422 FLOW_INPUT_SCHEMA_INVALID, 400 FLOW_FAILED for a run that ran and was rejected — see Run a flow via API

The automation dispatcher also exposes flow CRUD (POST /automation, GET/PUT/DELETE /automation/:name) and run observability/resume routes — see Durable pause & resume.

To list flows, read the metadata plane: GET /meta/flow (client.meta.getItems('flow')) — flows are metadata (ADR-0106). There is no GET /automation list route; it was retired, and a GET to that path gets the host's method-mismatch answer (405 with Allow: POST on the default server), because only POST lives there. Per-flow runtime enablement and trigger binding is GET /automation/_status (client.automation.getRuntimeStatus()).

Views (/ui) — Plugin Required

Not yet mounted. The `/ui/views` CRUD routes are declared in the API protocol but no handler is registered for them, so they return **404** today (and the `/api/v1/ui` prefix is itself deprecated in favour of `/api/v1/meta/view`). The always-available view **resolver** is a separate route — see the note below.
Method Endpoint Description
GET /ui/views/:object List views for an object
GET /ui/views/:object/:viewId Get a view definition
POST /ui/views/:object Create a new view
PATCH /ui/views/:object/:viewId Update a view
DELETE /ui/views/:object/:viewId Delete a view
The auto-generated (non-CRUD) view resolver `GET /ui/view/:object/:type` is always available and provided by the kernel — see [Metadata API](/docs/api/metadata-api). It is a separate route from the `/ui/views` CRUD above.

Realtime (/realtime) — Plugin Required

Not implemented. There is no realtime HTTP or WebSocket surface: `@objectstack/service-realtime` is an in-process pub/sub bus consumed via `kernel.getService('realtime')`, not over HTTP. The dispatcher has no `/realtime` branch, discovery deliberately omits the route (`features.websockets` is `false`), and the routes below would return **404** (ADR-0076 D12). They document the planned protocol shape only.
Method Endpoint Description
POST /realtime/connect Establish WebSocket/SSE connection
POST /realtime/disconnect Close connection
POST /realtime/subscribe Subscribe to channel
POST /realtime/unsubscribe Unsubscribe
PUT /realtime/presence/:channel Set presence
GET /realtime/presence/:channel Get channel presence

Notifications (/notifications) — Plugin Required

Method Endpoint Description
GET /notifications List notifications (inbox)
POST /notifications/read Mark notifications as read (body: { ids: string[] })
POST /notifications/read/all Mark all as read
The core dispatcher implements only the list / read / read-all routes above. Device-registration and preference endpoints are provided by specific notification plugins, if any.

AI (/ai) — Plugin Required

These are the routes service-ai mounts, and the SDK method that reaches each:

Method Endpoint SDK Description
POST /ai/chat ai.chat / ai.chatStream Chat completion — JSON with stream: false, otherwise the Vercel UI Message Stream
POST /ai/chat/stream — Generic-SSE twin of /ai/chat: the same completion without the tool loop or persistence
POST /ai/complete ai.complete Text completion
GET /ai/models ai.models Models this system offers (cloud ADR-0028)
GET /ai/status — Active adapter provenance (console diagnostics)
GET /ai/effective-model — Resolved model ids and their source (console diagnostics)
POST / GET /ai/conversations ai.conversations.create / .list Create / list conversations — the list is newest first and pages by keyset: send the id of the last conversation you hold as cursor, and read hasMore to learn whether another page follows
GET / PATCH / DELETE /ai/conversations/:id ai.conversations.get / .update / .delete Read / update / delete
POST /ai/conversations/:id/messages ai.conversations.addMessage Append a message
**This table used to be inverted (#3718).** It listed `/ai/nlq`, `/ai/suggest` and `/ai/insights` — none of which any plugin has ever mounted — and its callout stated "there is no `/ai/chat` route", which was wrong.

The three phantom routes were declared in DEFAULT_AI_ROUTES and called by client.ai.nlq/suggest/insights; every call 404ed. v17 removed that SDK namespace rather than build endpoints for it, and the same issue then gave the SDK the surface that does exist — the ai.* column above.

Reviewed dispositions for all 12 routes live in the cloud repo, packages/service-ai/src/ai-route-ledger.ts, whose conformance test reads buildAIRoutes() and drives the SDK against it. The three rows with no SDK method are deliberate: /status and /effective-model are operator diagnostics, and /chat/stream is superseded by /chat's streaming mode. For a React chat UI prefer useChat() (@ai-sdk/react) — it speaks the same protocol ai.chatStream parses and owns message state for you.

i18n (/i18n) — Plugin Required

Method Endpoint Description
GET /i18n/locales List available locales
GET /i18n/translations/:locale Get translation bundle
GET /i18n/labels/:object/:locale Get field labels

File Storage (/storage) — Plugin Required

Provided by @objectstack/service-storage, which registers these routes on the host HTTP server. Uploads are a three-step protocol — ask for a presigned target, send the bytes straight to it, then commit — so the bytes never proxy through the API server, and the same shape works for S3, local disk, or any other adapter.

Method Endpoint SDK Description
POST /storage/upload/presigned storage.getPresignedUrl Step 1 — mint an upload target for a new sys_file row
POST /storage/upload/complete storage.upload Step 3 — commit the uploaded bytes; with attachTo, also attach the file to a record as the caller
POST /storage/upload/chunked storage.initChunkedUpload Open a chunked / resumable session
PUT /storage/upload/chunked/:uploadId/chunk/:chunkIndex storage.uploadPart Send one chunk
POST /storage/upload/chunked/:uploadId/complete storage.completeChunkedUpload Assemble the parts
GET /storage/upload/chunked/:uploadId/progress storage.resumeUpload Read session progress (first step of a resume)
GET /storage/files/:fileId/url storage.getDownloadUrl Resolve a short-lived signed download URL
GET /storage/files/:fileId — Stable browser URL; 302s to the same signed URL (what file fields carry)

storage.upload(file) runs steps 1–3 for you, including the direct-to-storage PUT in step 2. Step 1 refuses an empty mimeType with 400 INVALID_REQUEST, so for a file whose type the browser does not know (file.type === '') the helper sends application/octet-stream; a typed file keeps its own type.

To attach the upload to a record's Attachments, send attachTo: { parentObject, parentId } with step 3 (and start the upload with scope: 'attachments'): the commit and the sys_attachment row are one unit, written as the signed-in caller, and the answer carries the attachmentId (attachment access). A refused attach leaves the upload pending.

A file field takes the fileId only after step 3. A signed-in record write that names a file whose upload was never completed is refused with 400 VALIDATION_FAILED, one invalid_value entry naming the field and the file, and the file is not claimed. Complete the upload, then save the record again.

**This table used to list two routes that never worked (#4087).** `POST /storage/upload` and `GET /storage/file/:id` were a dispatcher-side bridge to the `file-storage` service, written against a service shape that does not exist: it called `upload(key, data, options?)` as `upload(file, { request })` — a `TypeError` against every implementation in the repo — and read the `Buffer` that `download(key)` resolves as if it were a `{ url | stream | mimeType }` descriptor. They stayed mounted and reachable — nothing shadowed them — so anyone who followed this table got a 500; everyone who followed the SDK used the protocol above and never touched them.

Both routes are retired. /storage is service-storage's surface — install that package to get it. Discovery advertises the route only when the occupant of the storage slot (spelled file-storage before #9683; that spelling stays a deprecated v17 alias) actually mounts HTTP handlers, so an in-memory dev implementation now reports handlerReady: false and no routes.storage instead of pointing at a path with nothing behind it.


See also

  • API Overview — discovery, error handling, and protocol types
  • Client SDK — service-aware TypeScript client that adapts to installed plugins
  • Declarative Endpoints — declaring an endpoint of your own (apis:), the other half of this page's name