Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion content/docs/ai/actions-as-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@ migrations) that genuinely need full authority opt in explicitly with
never the consequence of a forgotten field.

```typescript
await aiService.chatWithTools(messages, tools, {
await aiService.chatWithTools(messages, {
tools,
toolExecutionContext: {
actor: {
id: currentUser.id,
Expand Down
6 changes: 4 additions & 2 deletions content/docs/ai/connect-mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ copy-paste-ready connect snippets per client), or over REST:

```bash
curl -b cookies.txt -X POST https://your-deployment.example.com/api/v1/keys
# → { "key": "osk_..." } — shown once; store it in your secret manager
# → { "success": true, "data": { "key": "osk_...", "prefix": "osk_...", "name": "API Key" } }
# the raw key is shown once — store it in your secret manager
```

Send it on every request in any of three equivalent forms:
Expand All @@ -87,11 +88,12 @@ run insecurely.

## What the agent gets

Ten data and action tools, generated from your metadata:
Eleven tools, generated from your metadata:

| Tool | What it does |
|:---|:---|
| `list_objects` / `describe_object` | Discover which objects exist and their fields |
| `validate_expression` | Check a CEL expression against an object's schema before authoring it into a formula, validation, or flow condition |
| `query_records` / `get_record` | Read data (list queries are capped at 50 rows per page by default) |
| `aggregate_records` | Grouped aggregation (registered when the active driver supports it) |
| `create_record` / `update_record` / `delete_record` | Write data |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/ai/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ flowchart TD

✅ **DO:**
- Declare knowledge sources via the Knowledge Protocol and pick the adapter (`memory`, `ragflow`, custom) that fits your data
- Rely on the built-in data tools (`query_records` / `get_record` / `aggregate_data`) for live record access
- Rely on the built-in data tools (`query_records` / `get_record` / `aggregate_records`) for live record access
- Let permission-aware retrieval and RLS scope what each user's agent can see
- Keep indexed objects in sync via the protocol's event sync

Expand Down Expand Up @@ -165,7 +165,7 @@ Server-side, the same flow runs through `chatWithTools`, threading the
end-user's `ExecutionContext` so tool calls respect row-level security:

```typescript
const reply = await aiService.chatWithTools(messages, tools, {
const reply = await aiService.chatWithTools(messages, {
toolExecutionContext: {
actor: { id: currentUser.id, name: currentUser.displayName, positions: currentUser.positions, permissions: currentUser.permissions },
conversationId,
Expand Down
6 changes: 3 additions & 3 deletions content/docs/api/client-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The `@objectstack/client` is the official TypeScript client for ObjectStack. It
- **Batch Operations**: Efficient bulk create/update/upsert/delete with transaction support
- **Query Builder**: Programmatic query construction with `createQuery()` and `createFilter()`
- **Standardized Errors**: Machine-readable error codes with retry guidance
- **Protocol Compliant**: Implements the core API namespaces defined in `@objectstack/spec`, plus additional namespaces (approvals, feed, organizations, projects/environments)
- **Protocol Compliant**: Implements the core API namespaces defined in `@objectstack/spec`, plus additional namespaces (approvals, organizations, projects/environments)

## Installation

Expand Down Expand Up @@ -76,7 +76,7 @@ async function main() {
When you call `client.connect()`, the client:

1. Probes `/api/v1/discovery` first, then falls back to `{origin}/.well-known/objectstack`
2. Parses the discovery response including `routes`, `features`, and `services`
2. Parses the discovery response including `routes`, `capabilities`, and `services`
3. Configures all API route paths dynamically

```typescript
Expand Down Expand Up @@ -104,7 +104,7 @@ if (discovery.services?.auth?.enabled) {

## Protocol Coverage

The `@objectstack/client` SDK aims to implement the ObjectStack API protocol specification. The core namespaces are listed below; the client also exposes additional namespaces (`approvals`, `feed`, `organizations`, `oauth`, `projects`/environments) — see [`index.ts`](https://github.com/objectstack-ai/framework/blob/main/packages/client/src/index.ts) for the full surface:
The `@objectstack/client` SDK aims to implement the ObjectStack API protocol specification. The core namespaces are listed below; the client also exposes additional namespaces (`approvals`, `organizations`, `oauth`, `projects`/environments) — see [`index.ts`](https://github.com/objectstack-ai/framework/blob/main/packages/client/src/index.ts) for the full surface:

| Namespace | Status | Methods | Purpose |
|:----------|:------:|:--------|:--------|
Expand Down
5 changes: 3 additions & 2 deletions content/docs/api/data-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,9 @@ or the analytics service's `cubes` config) — a cube referenced by a query that
yet registered is lazily auto-inferred from that query's shape, but metadata isn't
proactively generated for every object.

**Response**: Array of cube definitions with measures and dimensions (time-based
dimensions are `dimensions` entries with `type: "time"`).
**Response**: `{ success: true, data: [...] }` where `data` is an array of cube
definitions with measures and dimensions (time-based dimensions are `dimensions`
entries with `type: "time"`).

### `POST /analytics/sql`

Expand Down
5 changes: 3 additions & 2 deletions content/docs/api/error-handling-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,9 @@ export const ValidateTaskData: Hook = {
## 4. Error Logging and Monitoring

There is no global `onError` lifecycle event — the hook `HookEvent` enum only
covers `before*` / `after*` of `find`, `insert`, `update`, `delete` (and their
bulk variants). Each `Hook` does expose an `onError` *strategy* field
covers `before*` / `after*` of `find`, `insert`, `update`, `delete`. Bulk
(`multi: true`) writes fire these same events, not separate `*Many` variants.
Each `Hook` does expose an `onError` *strategy* field
(`'abort'` — the default — or `'log'`) that decides whether a handler failure
aborts the operation or is merely logged. For structured error logging, wrap the
risky work in a try/catch inside the handler and log there:
Expand Down
4 changes: 2 additions & 2 deletions content/docs/api/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Returns the full discovery manifest.
**Response**:
```json
{
"version": "1.0.0",
"version": "v1",
"apiName": "ObjectStack API",
"routes": {
"data": "/api/v1/data",
Expand All @@ -124,7 +124,7 @@ Returns the full discovery manifest.
"auth": { "enabled": false, "status": "unavailable", "message": "Install plugin-auth to enable" }
},
"capabilities": {
"feed": { "enabled": false },
"cron": { "enabled": false },
"automation": { "enabled": false },
"search": { "enabled": false }
}
Expand Down
12 changes: 12 additions & 0 deletions content/docs/api/plugin-endpoints.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ The following endpoints become available when the corresponding plugin is instal

### Workflow (`/workflow`) — Plugin Required

<Callout type="warn">
Not yet mounted. These routes are declared in the API protocol but the core dispatcher registers no `/workflow` handler and no bundled plugin provides a `workflow` service (only an in-memory dev stub), so they return **404** today. `discovery.services.workflow` reports `unavailable` in a standard install.
</Callout>

| Method | Endpoint | Description |
|:-------|:---------|:------------|
| GET | `/workflow/:object/config` | Get workflow configuration |
Expand All @@ -46,6 +50,10 @@ The automation dispatcher also exposes flow CRUD (`GET`/`POST /automation`, `GET

### Views (`/ui`) — Plugin Required

<Callout type="warn">
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.
</Callout>

| Method | Endpoint | Description |
|:-------|:---------|:------------|
| GET | `/ui/views/:object` | List views for an object |
Expand All @@ -60,6 +68,10 @@ The auto-generated (non-CRUD) view resolver `GET /ui/view/:object/:type` is alwa

### Realtime (`/realtime`) — Plugin Required

<Callout type="warn">
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.
</Callout>

| Method | Endpoint | Description |
|:-------|:---------|:------------|
| POST | `/realtime/connect` | Establish WebSocket/SSE connection |
Expand Down
6 changes: 1 addition & 5 deletions content/docs/api/wire-format.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,6 @@ The response is the `DeleteDataResponse` envelope: `{ object, id, success }`.
}
```

<Callout type="info">
**Soft Delete:** If the object enables soft delete (`softDelete: { enabled: true }` in its definition), the record is moved to the trash / recycle bin instead of being permanently deleted.
</Callout>

---

## 6. Get Metadata
Expand Down Expand Up @@ -365,7 +361,7 @@ Field-level failures carry a `fields` array.

```json
{
"error": "[Security] Access denied: operation 'update' on object 'task' is not permitted for roles [standard_user]",
"error": "[Security] Access denied: operation 'update' on object 'task' is not permitted for positions [standard_user]",
"code": "PERMISSION_DENIED",
"object": "task"
}
Expand Down
2 changes: 1 addition & 1 deletion content/docs/automation/approvals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A flow declares `runAs` (ADR-0049), and for approvals this is the decision that
- `runAs: 'user'` (default) — the flow's data operations run as the **submitter**, respecting their RLS. Good when the flow only touches records the submitter can already see.
- `runAs: 'system'` — **elevated**, bypasses RLS. Needed when the flow must read/write records the submitter can't (e.g. post to a ledger, notify an approver who owns rows the submitter can't see). Declare it **explicitly** so the elevation is visible, not accidental.

A schedule-triggered escalation has no triggering userso it must be `system` to act at all.
A schedule-triggered escalation has no triggering user, so under the default `runAs: 'user'` its data operations run **unscoped** (elevated, RLS-bypassing) anyway — declare `system` to make that elevation explicit and intended rather than an implicit fail-open (the engine warns, and `os lint` flags the bare shape as `flow-schedule-runas-unscoped`).

### 3. The approval node

Expand Down
2 changes: 1 addition & 1 deletion content/docs/automation/flows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ export const hotLeadFollowUp: Flow = {
type: 'create_record',
label: 'Create Follow-up Task',
config: {
object: 'task',
objectName: 'task',
fields: {
subject: 'Follow up on hot lead',
related_to: '{record.id}',
Expand Down
2 changes: 1 addition & 1 deletion content/docs/automation/hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ ctx = {
input, // mutable input — record fields exposed flat (raw wrapper: input.data, input.options)
result, // mutable operation result (after* events)
previous, // record state before the operation (update/delete)
session, // { userId, tenantId, roles, accessToken, isSystem }
session, // { userId, tenantId, positions, accessToken, isSystem }
user, // { id, name, email } convenience shortcut — reserved for future use;
// not currently set by the engine, use session.userId instead
transaction, // active transaction handle, if any
Expand Down
12 changes: 6 additions & 6 deletions content/docs/automation/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,16 @@ in `definition_json`, a serialised `Webhook` JSON (canonical schema:
| `name` | text | Unique snake_case name — referenced in logs and audit. |
| `label` | text | Optional display label. |
| `object_name` | text | Short object name whose record events fire this webhook. |
| `triggers` | text | Comma-separated event list: `create,update,delete`. |
| `triggers` | select | Multi-select of `create` / `update` / `delete`, stored as an array (the enqueuer also accepts a legacy comma-separated string). |
| `url` | text | External endpoint that receives the POST. |
| `method` | text | HTTP method. Default `POST`. |
| `method` | select | HTTP method — one of `GET` / `POST` / `PUT` / `PATCH` / `DELETE`. Default `POST`. |
| `description` | textarea | Free-text description. |
| `active` | boolean | Inactive webhooks are skipped by the dispatcher. Default `true`. |
| `definition_json` | textarea | Serialised `Webhook` JSON (`WebhookSchema` from `@objectstack/spec/automation`) — carries the full headers / auth / retry / payload config, including the signing `secret`, custom `headers`, and `timeoutMs`. |
| `created_at` | datetime | Standard audit columns. |
| `updated_at` | datetime | |

Matching at runtime is purely `object_name` + the comma-separated `triggers`
Matching at runtime is purely `object_name` + the multi-select `triggers`
list; the headers, signing secret, and per-attempt timeout are parsed out of
`definition_json` when an event is enqueued. There is no per-row org/tenant
column, no `events[]` glob field, no stored `retry_policy`, and no
Expand Down Expand Up @@ -167,7 +167,7 @@ Five stages, each implemented as a thin layer over an existing primitive.
└──────────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 2. Match subscriber filters sys_webhook by events[]/object, │
│ 2. Match subscriber filters sys_webhook by triggers/object, │
│ multiplies one event into N delivery rows │
└──────────────────────────┬──────────────────────────────────────┘
Expand Down Expand Up @@ -231,7 +231,7 @@ For each incoming event the subscriber:

1. Loads `sys_webhook` rows where `active = true` AND `object_name`
matches the event's object AND the event's action is in the row's
comma-separated `triggers`.
`triggers`.
2. For each match, builds the payload (§5).
3. Enqueues a `sys_http_delivery` row (`source = 'webhook'`).

Expand Down Expand Up @@ -470,7 +470,7 @@ packages/plugins/plugin-webhooks/
├── sys-webhook.object.ts # ObjectSchema.create() — sys_webhook config
├── auto-enqueuer.ts # realtime data.record.* → enqueue onto outbox
├── auto-enqueuer.test.ts # tests are colocated in src/, not a separate test/ dir
└── schema.ts # shared types
└── schema.ts # public re-export barrel (exports SysWebhook)
```

The delivery runtime — `sys_http_delivery`, `HttpDispatcher`, the HTTP sender,
Expand Down
2 changes: 1 addition & 1 deletion content/docs/concepts/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ From this single definition, ObjectStack automatically:
### The Three Truths

1. **The UI is a Projection** — The form is generated from the schema, not hand-coded
2. **The API is a Consequence** — REST/GraphQL endpoints appear automatically from object definitions
2. **The API is a Consequence** — REST endpoints appear automatically from object definitions
3. **The Schema is the Application** — Your entire business logic lives in metadata files

### When to Use Metadata-Driven
Expand Down
2 changes: 1 addition & 1 deletion content/docs/concepts/metadata-driven.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ Follow these strict naming conventions for consistency:
| **Object Names** (machine names) | `snake_case` | `todo_task`, `project_milestone`, `user_profile` |
| **Field Names** (machine names) | `snake_case` | `first_name`, `annual_revenue`, `is_active` |
| **Constant Names** (exports) | `PascalCase` | `TodoTask`, `ProjectMilestone`, `UserProfile` |
| **Configuration Keys** (props) | `camelCase` | `maxLength`, `defaultValue`, `referenceFilters` |
| **Configuration Keys** (props) | `camelCase` | `maxLength`, `defaultValue`, `lookupFilters` |

**Example:**
```typescript
Expand Down
2 changes: 1 addition & 1 deletion content/docs/concepts/metadata-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ In shared-database multi-tenancy, **most metadata types must not be per-org cust
| `view`, `dashboard`, `report`, `email_template` | ✅ | Pure rendering. Per-org customization is safe. |
| `flow` | ✅ | Per-org overlays are allowed for automation definitions. |
| `agent` | ❌ | Agents are platform-owned and closed to third parties (ADR-0063 §2) — no per-org agent fork. |
| `permission`, `role`, `profile` | ✅ | Per-org overlays are allowed; tenant-level controls layer on top. |
| `permission`, `position` | ✅ | Per-org overlays are allowed; tenant-level controls layer on top. |
| `object`, `field` | ❌ | Defines the table schema. Overriding would break existing data. |
| `datasource` | ❌ | Connection strings; multi-tenant isolation is enforced at a higher layer. |

Expand Down
15 changes: 9 additions & 6 deletions content/docs/data-modeling/analytics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const SalesDataset = defineDataset({
// Aggregatable values — defined ONCE here; referenced everywhere by name.
measures: [
{ name: 'opp_count', aggregate: 'count' },
{ name: 'revenue', aggregate: 'sum', field: 'amount', format: '$0,0', certified: true },
{ name: 'revenue', aggregate: 'sum', field: 'amount', format: '$0,0' },
{ name: 'won_amount', aggregate: 'sum', field: 'amount', filter: { stage: 'closed_won' } },
// Derived measure — references OTHER measures by name only (no raw fields/SQL).
{ name: 'win_rate', derived: { op: 'ratio', of: ['won_amount', 'revenue'] }, format: '0.0%' },
Expand All @@ -81,8 +81,9 @@ export default defineStack({

- **No raw SQL, no hand-authored joins.** The author declares *which*
relationships to include; the compiler derives the join from the object graph.
- **`certified: true`** marks a human-blessed metric — the review checkpoint.
Reviewing AI output collapses to "did it use certified measures correctly."
- **Metric certification** — a `certified` flag that marks a measure as a
human-blessed governance checkpoint — is a design goal of ADR-0021 but is **not
yet implemented**; `DatasetMeasureSchema` has no `certified` field today.
- **Derived measures** are first-class but *closed*: they reference other
measures by name only (`ratio` / `sum` / `difference` / `product`).
- **RLS / tenant scoping is enforced by the runtime**, per joined object — never
Expand Down Expand Up @@ -191,6 +192,8 @@ steps so it could be verified safely:
presentation schemas themselves were kept — only their inline query fields
went away.

Author new analytics directly in dataset form; reach for a **named** dataset when
a metric is shared or must be certified, and an inline anonymous dataset for a
one-off single-object KPI.
Author new analytics directly in dataset form: define the metric once as a
**named** dataset registered in your stack, then bind reports, dashboard widgets,
and list-view charts to it **by name**. Presentations reference a dataset by its
`name` string — the shipped presentation schemas carry no inline/anonymous
dataset shape, so even a one-off single-object KPI is a small named dataset.
4 changes: 3 additions & 1 deletion content/docs/data-modeling/drivers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ Drivers can be selected in two ways:

<Callout type="info">
Turso / libSQL (`libsql://`, `*.turso.io`) is **not** supported by the
open-source framework — no bundled driver dispatches `libsql://` URLs.
open-source framework. The CLI recognizes these URLs (mapping them to a
`turso` driver kind), but no bundled driver implements it — so they never
actually connect to Turso.
</Callout>

## Supported Drivers
Expand Down
Loading