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
6 changes: 6 additions & 0 deletions .changeset/docs-skills-sync-9-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
---

docs(skills): sync objectstack-* skills with 9.0→9.4 changes. Documentation-only
update to the `skills/` authoring guides — no package version impact (empty
changeset to satisfy the changeset check).
40 changes: 37 additions & 3 deletions skills/objectstack-ai/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,35 @@ Agent → Skill → Tool
> agents is supported but considered legacy. Skills provide better
> discoverability, instruction scoping, and reuse.

### Built-in Unified Assistant (ADR-0040)

The runtime ships a unified persona agent **`data_chat`** (`DEFAULT_DATA_AGENT_NAME`)
— the implicit default copilot for any app that doesn't pin `app.defaultAgent`
(Studio overrides to `metadata_assistant`). It attaches built-in skills:

- **`data_explorer`** — read-only data Q&A: `list_objects`, `describe_object`,
`query_data` / `query_records`, `get_record`, `aggregate_data`, `visualize_data`.
- **`metadata_authoring`** + **`solution_design`** — app-building, **cloud/EE only**.

To grant data exploration to your own agent, add `data_explorer` to its `skills[]`;
deactivating that skill (`active: false`) revokes data Q&A for every agent that
references it.

> **Edition gate (#1803):** when neither `metadata_authoring` nor `solution_design`
> is registered — the open single-env framework — the assistant is **query-only**
> and declines build/authoring requests. App-building tools are supplied entirely
> by the cloud AI Studio plugin; do not assume they resolve in the open framework.

> **`visualize_data` (#1820/#1821):** the only built-in tool that draws a chart —
> it aggregates an object and emits an inline `data-chart` part. Auto-registered
> **only** when an analytics service (`IAnalyticsService`) is wired; `query_data` /
> `aggregate_data` return numbers, not charts.

> **Ops:** set `AI_DAILY_USER_MESSAGES=<N>` to cap user turns per user per day
> (ADR-0040 §5; backed by the `ai_usage_daily` object, no-op if unset). Adapter
> health is observable at `GET /api/v1/ai/status`; invalid `ai` settings are
> rejected at save time (#1788).

---

## Agent Configuration
Expand Down Expand Up @@ -110,9 +139,9 @@ export default defineAgent({
temperature: 0.3,
},
guardrails: {
blockedTopics: ['internal_pricing', 'employee_data'],
maxTurns: 20,
requireApprovalFor: ['delete_record', 'escalate'],
blockedTopics: ['internal_pricing', 'employee_data'], // forbidden topics / action names
maxTokensPerInvocation: 8000, // token budget per invocation
maxExecutionTimeSec: 60, // wall-clock cap per invocation
},
});
```
Expand Down Expand Up @@ -322,6 +351,11 @@ Retrieval-Augmented Generation gives agents access to domain knowledge.
| `azure_openai` | Same as OpenAI, enterprise managed | Compliance, data residency |
| `local` | Ollama, vLLM, llama.cpp | On-premise, air-gapped |

> The inline agent `model.provider` enum is the narrow set above
> (`openai` / `azure_openai` / `anthropic` / `local`). **Model-registry** entries
> (`ModelProviderSchema`) accept a wider set: also `google`, `cohere`,
> `huggingface`, `custom`.

### Model Selection Guidelines

| Scenario | Recommended |
Expand Down
23 changes: 23 additions & 0 deletions skills/objectstack-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ GET /api/v1/{object}/aggregate # Aggregation queries
> **Key rule:** If your object defines `apiMethods`, only those operations are
> exposed. For example, `apiMethods: ['get', 'list']` creates a read-only API.

### Metadata API (`/meta`)

The metadata read surface lives under `/api/v1/meta` (separate from the data
CRUD routes above):

```
GET /api/v1/meta/:type # List metadata items of a type (object, view, flow, doc, …)
GET /api/v1/meta/:type/:name # Read a single metadata item
```

Three query-param contracts gained in 9.x:

- **`?preview=draft`** (#1763) — overlay pending **draft** metadata instead of the
published copy, on both list and get. The draft path is **cache-bypassed**, so
it always reflects the latest unpublished edit (ADR-0033/0037 authoring loop).
- **`?package=<packageId>`** (ADR-0048, #1816/#1819) — **package-scope** a read so
two installed packages that share a bare metadata name disambiguate by owning
package; prefer-local resolution. A package-scoped read **bypasses the meta
cache**. The layered / Studio-editor read is package-scoped the same way.
- **`/meta/doc`** (ADR-0046, #1790) — docs-as-metadata. The **list** response omits
each doc's `content` by default (use `?include=content` to include it); the
**single-item** `GET /meta/doc/:name` always returns the full body.

### Public (anonymous) Form Endpoints

Any `FormView` declared with `sharing.allowAnonymous: true` and a
Expand Down
36 changes: 32 additions & 4 deletions skills/objectstack-automation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ parallel. Flows are the primary automation building block in ObjectStack.
| `screen` | Interactive — presents UI screens to the user (wizards, forms) |
| `schedule` | Runs on a cron schedule (daily cleanup, weekly reports) |
| `record_change` | Fires automatically on record create/update/delete (bind via the `start` node's `triggerType`) |
| `api` | Invoked explicitly via the API / `engine.execute()` |
| `api` | Invoked explicitly via the API / `engine.execute()`, **or** bound as an inbound **webhook**: `POST /api/v1/automation/hooks/:flowName/:hookId` (see *Inbound webhook triggers* below) |

### Flow Node Types

Expand Down Expand Up @@ -342,6 +342,7 @@ branch — you never resume the flow by hand.
| `lockRecord` | Lock the triggering record from edits while pending. Default `true` |
| `approvalStatusField` | Business-object field to mirror `pending`/`approved`/`rejected`/`recalled` onto (should be readonly) |
| `escalation` | Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }` |
| `maxRevisions` | ADR-0044 — max **send-backs-for-revision** per run before auto-reject. Default `3`; `0` disables send-back. Only meaningful when the node has a `revise` out-edge |

### Branching, side-effects & rejection

Expand All @@ -353,6 +354,12 @@ These are wired on the **graph**, not in node config:
`http_request`, an email node, …) to the `approve` / `reject` out-edge.
- **Roll back on reject** — route the `reject` edge as a **back-edge** to an
earlier node so the submitter can revise (the old `back_to_previous`).
- **Send back for revision (ADR-0044)** — distinct from a plain reject: an
Approval node can emit a third decision **`revise`** on a `revise`-labeled
out-edge that routes to a rework wait point. The submitter edits and
resubmits, re-entering the node via an edge `type: 'back'` (a declared
back-edge — traversed at run time but excluded from DAG cycle validation).
`maxRevisions` (node config, default `3`) caps the loop before auto-reject.
- **Hard reject** — route the `reject` edge to an `end` node (the old
`reject_process`).

Expand Down Expand Up @@ -381,17 +388,38 @@ ObjectQL lifecycle hook.

### Prerequisite — enable the `triggers` capability

Record-change (and schedule) triggers ship as a plugin gated behind the
Record-change, schedule, **and inbound-webhook (`api`)** triggers ship behind the
`triggers` capability. **Without it the flows register but never fire.** Add it
to the package config (pair with `job` for schedule/cron triggers):
to the package config:

```typescript
defineStack({
// …
requires: ['automation', 'triggers'], // + 'job' for scheduled flows
requires: ['automation', 'triggers'],
// + 'job' for scheduled (cron) flows
// + 'queue' for inbound-webhook ('api') flows — the trigger-api plugin
// depends on the queue service; without it every inbound POST
// returns 503 queue_unavailable.
});
```

### Inbound webhook (`api`) triggers (ADR-0041 Tier 1)

An `api` flow can be bound to an inbound HTTP endpoint:
`POST /api/v1/automation/hooks/:flowName/:hookId`. Configure it on the **start
node `config`** (the start `config` is a free-form record, so these keys are
read at runtime, not Zod-validated):

| `config` key | Purpose |
|:-------------|:--------|
| `hookId` | URL path token (default `'default'`). **Rotate it to revoke** a leaked endpoint |
| `secret` | HMAC-SHA256 shared secret. Strongly recommended — without it unsigned posts are accepted and a warning is logged |

- **Signature:** sender sends `x-objectstack-signature: sha256=<hex>` (GitHub/Stripe style).
- **Idempotency:** `x-idempotency-key` dedupes retries — author the flow to be idempotent (delivery is at-least-once).
- **Queue-backed:** the endpoint ACKs `202` and enqueues; the flow runs on the consumer, never in-band. Requires the `queue` service (see prerequisite).
- The JSON body surfaces to the flow as the trigger record (`record.*` / bare fields) plus `params`.

### Trigger Types (start-node `config.triggerType`)

| `triggerType` | Fires | ObjectQL hook |
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,12 @@ For `lookup` fields, supply the **natural key** of the target record (not
its UUID). The seed runner resolves at load time. Order datasets so parents
appear before children in the exported array:

> If a lookup value matches no natural key, the loader now falls back to
> resolving it as the target's `id` (#1814) — so a reference to a real existing
> record by internal id resolves instead of dangling to null. Natural keys
> remain the portable default; rely on the id fallback only for records you
> didn't seed (e.g. a system user).

```typescript
const contacts = defineDataset(Contact, {
externalId: 'email',
Expand Down
29 changes: 25 additions & 4 deletions skills/objectstack-platform/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,9 @@ manifest: {
}
```

**Object naming:** The object `name` is the canonical identifier and equals the physical table name. If you want a domain prefix, embed it directly in the name (e.g. `name: 'crm_account'`). There is no automatic namespace prefixing.
**Object naming:** The object `name` is the canonical identifier and equals the physical table name. Embed any domain prefix directly in the name (e.g. `name: 'crm_account'`); the object-level `namespace` *field* is deprecated and ignored by the runtime.

**`manifest.namespace` (ADR-0048):** Optional, but **enforced once set**. When a package declares `manifest.namespace: 'crm'`, every `object.name` must start with `crm_` or `defineStack` errors (`validateNamespacePrefix` in `@objectstack/spec`); the legacy `<ns>__<short>` double-underscore form is rejected, and `sys_`-prefixed names are platform-reserved and exempt. The namespace is also a package-ownership key — installing two packages that both claim `crm` fails with `NamespaceConflictError` (downgrade to a warning with `OS_METADATA_COLLISION=warn`). `os lint` additionally emits a non-fatal `naming/namespace-prefix` warning for bare-named UI/automation items (app, page, dashboard, flow, action, report, dataset) when a namespace is set.

---

Expand Down Expand Up @@ -419,12 +421,14 @@ CLI: `os serve` / `os dev`
├── AppPlugin (loads the defineStack bundle)
├── I18nServicePlugin (if translations/i18n defined)
├── AuthPlugin
├── Split platform-app plugins (ADR-0048, optional/best-effort, after AuthPlugin):
│ @objectstack/setup → createSetupAppPlugin (first-run wizard)
│ @objectstack/studio → createStudioAppPlugin
│ @objectstack/account → createAccountAppPlugin
├── HonoServerPlugin
├── SetupPlugin (first-run wizard)
├── RESTPlugin (auto-generated API)
├── DispatcherPlugin
├── AIServicePlugin (if available)
└── StudioPlugin (if --ui flag)
└── AIServicePlugin (if available)
5. Runtime.start() → init + start all plugins
6. Server listens on the resolved port (see "Ports & networking" in Part 3)
```
Expand Down Expand Up @@ -958,6 +962,10 @@ describe('AuditPlugin', () => {
| `com.objectstack.metadata` | `metadata` | `@objectstack/metadata` |
| `com.objectstack.realtime` | `realtime` | `@objectstack/service-realtime` |
| `com.objectstack.cache` | `cache` | `@objectstack/service-cache` |
| `com.objectstack.setup` | — | `@objectstack/setup` → `createSetupAppPlugin` (ADR-0048 one-app pkg) |
| `com.objectstack.studio` | — | `@objectstack/studio` → `createStudioAppPlugin` |
| `com.objectstack.account` | — | `@objectstack/account` → `createAccountAppPlugin` |
| `com.objectstack.cloud-connection` | — | `@objectstack/cloud-connection` → `createCloudConnectionPlugin` |

---

Expand All @@ -968,6 +976,8 @@ metadata is read-only and artifact/file backed:

- Do **not** register `sys_metadata` or `sys_metadata_history` from an ObjectOS
runtime plugin. Those persistence tables belong to the control plane.
(Exception, #1826: an *isolated project kernel* may opt into `sys_metadata`
hydration from its own DB — the general boundary otherwise stands.)
- Do **not** call `MetadataManager.setDataEngine()` automatically from
`MetadataPlugin.start()`. Project databases must contain business rows only.
- Use `artifactSource: { mode: 'local-file', path: './dist/objectstack.json' }`
Expand Down Expand Up @@ -1124,6 +1134,17 @@ Port resolution is the same for `os dev` and `os start` (both spawn `os serve`):
| `os publish` | Push the compiled stack to a cloud environment |
| `os register` | Register the local stack as a deployable target |
| `os cloud …` | Cloud-specific subcommands (logs, metrics, status) |
| `os package publish [dist/objectstack.json] [--env … --install --visibility org]` | Upload the compiled artifact as a versioned package to the cloud catalog (ADR-0008 P3) |
| `os package install <manifest-id │ ./dist/objectstack.json> [--version │ --runtime http://localhost:3000]` | Install a package into a **running** runtime via its install-local endpoint. Catalog mode (by manifest id) or air-gapped local-artifact mode. Auths with the **target runtime's** session (`--email/--password` or `OS_RUNTIME_EMAIL`/`OS_RUNTIME_PASSWORD`), not the cloud login |

> **Cloud connection & marketplace (`@objectstack/cloud-connection`, ADR-0008/0009).**
> The open runtime-side cloud client. Its plugins —
> `CloudConnectionPlugin`/`createCloudConnectionPlugin`, `MarketplaceProxyPlugin`,
> `MarketplaceInstallLocalPlugin`, `RuntimeConfigPlugin` — expose the install-local
> endpoint that `os package install` targets, ship the **Installed Apps** page and
> marketplace Setup nav as plugin metadata, and maintain `LocalManifestSource`
> (a local desired-state ledger) plus runtime-identity bind v2 (environment-less
> self-hosted binding).

## Testing pattern

Expand Down
43 changes: 38 additions & 5 deletions skills/objectstack-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ Rules:
so most views need no filter elements at all.
- `userFilters: { element: 'dropdown' }` (no `fields`) is valid shorthand:
the renderer fills the field list from the object's select/boolean fields.
- `element` is `dropdown` or `tabs`; `toggle` is **deprecated** (ADR-0047 §3.4a)
— it stays in the enum for back-compat rendering, but author `dropdown`/`tabs`.
- The visualization switcher renders as a compact dropdown in the toolbar's
right cluster. Authors only control the `allowedVisualizations` whitelist;
a single-entry whitelist locks the visualization (no switcher).
Expand Down Expand Up @@ -328,6 +330,9 @@ export const CrmApp = App.create({
label: 'Enterprise CRM',
icon: 'briefcase',
defaultAgent: 'sales_copilot', // optional AI copilot binding
// hidden: true, // ADR-0045 — drop from the App Switcher but keep
// routable & permission-checked; the shell surfaces
// hidden apps (e.g. `account`) via the avatar menu.
branding: {
primaryColor: '#4169E1',
logo: '/assets/crm-logo.png',
Expand Down Expand Up @@ -440,7 +445,7 @@ filter; keep `filter` on the widget when binding a dataset.
|:-----|:------------|
| `tabular` | Flat data table with columns and filters |
| `summary` | Grouped data with subtotals (e.g., revenue by region) |
| `matrix` | Cross-tab / pivot table (two grouping dimensions) |
| `matrix` | Cross-tab / pivot table (`rows` down × `columns` across) |
| `chart` | Visual chart report |
| `joined` | Multi-block analytic surface (combines several sub-reports) |

Expand All @@ -458,9 +463,11 @@ export const PipelineCoverageReport: ReportInput = {
label: 'Pipeline Coverage (Quarter)',
type: 'matrix',
dataset: 'opportunity_metrics',
rows: ['forecast_category', 'close_date'],
values: ['amount_sum'],
rows: ['forecast_category'], // down axis
columns: ['close_date'], // across axis (ADR-0021 D2) — matrix pivots rows × columns
values: ['amount_sum'], // measures placed in the cells
runtimeFilter: { stage: { $ne: 'closed_lost' } },
// drilldown defaults true — click a cell to open the underlying records; set false to disable.
chart: { type: 'bar', xAxis: 'forecast_category', yAxis: 'amount_sum' },
};
```
Expand All @@ -470,8 +477,11 @@ export const PipelineCoverageReport: ReportInput = {
> field server-side in a single aggregate query — do **not** pre-compute virtual
> columns for this.
> **`rows`** are the report's grouping dimensions (selected from the dataset by
> name). A `summary` groups by them; a `matrix` cross-tabs them. Multi-level
> grouping = multiple dimension names in the array.
> name). A `summary` groups *down* by `rows`. A `matrix` pivots `rows` (down) ×
> **`columns`** (across, ADR-0021 D2) with `values` in the cells — do **not**
> put both axes in `rows`. Multi-level grouping on either axis = multiple
> dimension names in that array. `drilldown` (default `true`) makes cells
> click-through to the underlying records.

---

Expand Down Expand Up @@ -1096,6 +1106,29 @@ export const AddToCampaignAction: Action = {
};
```

### Opening in a New Tab (`opensInNewTab` / `newTabUrl`)

For actions that should land in a new browser tab, set `opensInNewTab: true`
(#1787). The renderer pre-opens the tab **synchronously** on click so popup
blockers don't fire, then navigates it to the handler's returned `redirectUrl`.

For external deep-links / SSO with no server round-trip, add `newTabUrl` — a
direct URL template (supports the `{recordId}` placeholder). It is valid **only**
alongside `opensInNewTab: true`, and the target endpoint must enforce its own
auth (the new tab carries no in-app session context).

```typescript
export const OpenInvoicePdfAction: Action = {
name: 'open_invoice_pdf',
label: 'Open PDF',
objectName: 'invoice',
type: 'url',
opensInNewTab: true,
newTabUrl: '/api/v1/invoice/{recordId}/pdf', // zero-roundtrip; endpoint self-auths
locations: ['record_header'],
};
```

### Action Parameter Patterns

Prefer **field-backed** params (`{ field: 'email' }`) over inline declarations
Expand Down