Skip to content

Commit 4fa9499

Browse files
os-zhuangclaude
andcommitted
docs(skills): sync objectstack-* skills with 9.0→9.4 changes
Audited all changes from @objectstack/types@9.0.1 to 9.4.0 and updated the six skills whose documented contracts had drifted or gained capabilities: - ui: fix matrix report example (rows down × columns across, ADR-0021 D2) + drilldown; add Action opensInNewTab/newTabUrl (#1787); App.hidden (ADR-0045); userFilters toggle deprecation - platform: correct namespace prose — manifest.namespace is enforced (ADR-0048, validateNamespacePrefix/NamespaceConflictError); split-app boot sequence; os package install/publish; cloud-connection; sys_metadata opt-in - automation: inbound webhook (api) triggers + queue prerequisite (ADR-0041); ADR-0044 send-back-for-revision (revise/back edges, maxRevisions) - api: document the /meta REST surface — ?preview=draft (#1763), ?package= (ADR-0048), /meta/doc content omission (ADR-0046) - ai: built-in data_chat assistant + visualize_data + query-only gate (ADR-0040); fix stale guardrails example keys; model-registry providers - data: seed lookup id-fallback (#1814) query/formula/i18n skills required no changes (no contract drift in range). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a4ca755 commit 4fa9499

6 files changed

Lines changed: 161 additions & 16 deletions

File tree

skills/objectstack-ai/SKILL.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,35 @@ Agent → Skill → Tool
5757
> agents is supported but considered legacy. Skills provide better
5858
> discoverability, instruction scoping, and reuse.
5959
60+
### Built-in Unified Assistant (ADR-0040)
61+
62+
The runtime ships a unified persona agent **`data_chat`** (`DEFAULT_DATA_AGENT_NAME`)
63+
— the implicit default copilot for any app that doesn't pin `app.defaultAgent`
64+
(Studio overrides to `metadata_assistant`). It attaches built-in skills:
65+
66+
- **`data_explorer`** — read-only data Q&A: `list_objects`, `describe_object`,
67+
`query_data` / `query_records`, `get_record`, `aggregate_data`, `visualize_data`.
68+
- **`metadata_authoring`** + **`solution_design`** — app-building, **cloud/EE only**.
69+
70+
To grant data exploration to your own agent, add `data_explorer` to its `skills[]`;
71+
deactivating that skill (`active: false`) revokes data Q&A for every agent that
72+
references it.
73+
74+
> **Edition gate (#1803):** when neither `metadata_authoring` nor `solution_design`
75+
> is registered — the open single-env framework — the assistant is **query-only**
76+
> and declines build/authoring requests. App-building tools are supplied entirely
77+
> by the cloud AI Studio plugin; do not assume they resolve in the open framework.
78+
79+
> **`visualize_data` (#1820/#1821):** the only built-in tool that draws a chart —
80+
> it aggregates an object and emits an inline `data-chart` part. Auto-registered
81+
> **only** when an analytics service (`IAnalyticsService`) is wired; `query_data` /
82+
> `aggregate_data` return numbers, not charts.
83+
84+
> **Ops:** set `AI_DAILY_USER_MESSAGES=<N>` to cap user turns per user per day
85+
> (ADR-0040 §5; backed by the `ai_usage_daily` object, no-op if unset). Adapter
86+
> health is observable at `GET /api/v1/ai/status`; invalid `ai` settings are
87+
> rejected at save time (#1788).
88+
6089
---
6190

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

354+
> The inline agent `model.provider` enum is the narrow set above
355+
> (`openai` / `azure_openai` / `anthropic` / `local`). **Model-registry** entries
356+
> (`ModelProviderSchema`) accept a wider set: also `google`, `cohere`,
357+
> `huggingface`, `custom`.
358+
325359
### Model Selection Guidelines
326360

327361
| Scenario | Recommended |

skills/objectstack-api/SKILL.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,29 @@ GET /api/v1/{object}/aggregate # Aggregation queries
5959
> **Key rule:** If your object defines `apiMethods`, only those operations are
6060
> exposed. For example, `apiMethods: ['get', 'list']` creates a read-only API.
6161
62+
### Metadata API (`/meta`)
63+
64+
The metadata read surface lives under `/api/v1/meta` (separate from the data
65+
CRUD routes above):
66+
67+
```
68+
GET /api/v1/meta/:type # List metadata items of a type (object, view, flow, doc, …)
69+
GET /api/v1/meta/:type/:name # Read a single metadata item
70+
```
71+
72+
Three query-param contracts gained in 9.x:
73+
74+
- **`?preview=draft`** (#1763) — overlay pending **draft** metadata instead of the
75+
published copy, on both list and get. The draft path is **cache-bypassed**, so
76+
it always reflects the latest unpublished edit (ADR-0033/0037 authoring loop).
77+
- **`?package=<packageId>`** (ADR-0048, #1816/#1819) — **package-scope** a read so
78+
two installed packages that share a bare metadata name disambiguate by owning
79+
package; prefer-local resolution. A package-scoped read **bypasses the meta
80+
cache**. The layered / Studio-editor read is package-scoped the same way.
81+
- **`/meta/doc`** (ADR-0046, #1790) — docs-as-metadata. The **list** response omits
82+
each doc's `content` by default (use `?include=content` to include it); the
83+
**single-item** `GET /meta/doc/:name` always returns the full body.
84+
6285
### Public (anonymous) Form Endpoints
6386

6487
Any `FormView` declared with `sharing.allowAnonymous: true` and a

skills/objectstack-automation/SKILL.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ parallel. Flows are the primary automation building block in ObjectStack.
5757
| `screen` | Interactive — presents UI screens to the user (wizards, forms) |
5858
| `schedule` | Runs on a cron schedule (daily cleanup, weekly reports) |
5959
| `record_change` | Fires automatically on record create/update/delete (bind via the `start` node's `triggerType`) |
60-
| `api` | Invoked explicitly via the API / `engine.execute()` |
60+
| `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) |
6161

6262
### Flow Node Types
6363

@@ -342,6 +342,7 @@ branch — you never resume the flow by hand.
342342
| `lockRecord` | Lock the triggering record from edits while pending. Default `true` |
343343
| `approvalStatusField` | Business-object field to mirror `pending`/`approved`/`rejected`/`recalled` onto (should be readonly) |
344344
| `escalation` | Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }` |
345+
| `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 |
345346

346347
### Branching, side-effects & rejection
347348

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

@@ -381,17 +388,38 @@ ObjectQL lifecycle hook.
381388

382389
### Prerequisite — enable the `triggers` capability
383390

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

388395
```typescript
389396
defineStack({
390397
//
391-
requires: ['automation', 'triggers'], // + 'job' for scheduled flows
398+
requires: ['automation', 'triggers'],
399+
// + 'job' for scheduled (cron) flows
400+
// + 'queue' for inbound-webhook ('api') flows — the trigger-api plugin
401+
// depends on the queue service; without it every inbound POST
402+
// returns 503 queue_unavailable.
392403
});
393404
```
394405

406+
### Inbound webhook (`api`) triggers (ADR-0041 Tier 1)
407+
408+
An `api` flow can be bound to an inbound HTTP endpoint:
409+
`POST /api/v1/automation/hooks/:flowName/:hookId`. Configure it on the **start
410+
node `config`** (the start `config` is a free-form record, so these keys are
411+
read at runtime, not Zod-validated):
412+
413+
| `config` key | Purpose |
414+
|:-------------|:--------|
415+
| `hookId` | URL path token (default `'default'`). **Rotate it to revoke** a leaked endpoint |
416+
| `secret` | HMAC-SHA256 shared secret. Strongly recommended — without it unsigned posts are accepted and a warning is logged |
417+
418+
- **Signature:** sender sends `x-objectstack-signature: sha256=<hex>` (GitHub/Stripe style).
419+
- **Idempotency:** `x-idempotency-key` dedupes retries — author the flow to be idempotent (delivery is at-least-once).
420+
- **Queue-backed:** the endpoint ACKs `202` and enqueues; the flow runs on the consumer, never in-band. Requires the `queue` service (see prerequisite).
421+
- The JSON body surfaces to the flow as the trigger record (`record.*` / bare fields) plus `params`.
422+
395423
### Trigger Types (start-node `config.triggerType`)
396424

397425
| `triggerType` | Fires | ObjectQL hook |

skills/objectstack-data/SKILL.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,12 @@ For `lookup` fields, supply the **natural key** of the target record (not
695695
its UUID). The seed runner resolves at load time. Order datasets so parents
696696
appear before children in the exported array:
697697

698+
> If a lookup value matches no natural key, the loader now falls back to
699+
> resolving it as the target's `id` (#1814) — so a reference to a real existing
700+
> record by internal id resolves instead of dangling to null. Natural keys
701+
> remain the portable default; rely on the id fallback only for records you
702+
> didn't seed (e.g. a system user).
703+
698704
```typescript
699705
const contacts = defineDataset(Contact, {
700706
externalId: 'email',

skills/objectstack-platform/SKILL.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,9 @@ manifest: {
297297
}
298298
```
299299

300-
**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.
300+
**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.
301+
302+
**`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.
301303

302304
---
303305

@@ -419,12 +421,14 @@ CLI: `os serve` / `os dev`
419421
├── AppPlugin (loads the defineStack bundle)
420422
├── I18nServicePlugin (if translations/i18n defined)
421423
├── AuthPlugin
424+
├── Split platform-app plugins (ADR-0048, optional/best-effort, after AuthPlugin):
425+
│ @objectstack/setup → createSetupAppPlugin (first-run wizard)
426+
│ @objectstack/studio → createStudioAppPlugin
427+
│ @objectstack/account → createAccountAppPlugin
422428
├── HonoServerPlugin
423-
├── SetupPlugin (first-run wizard)
424429
├── RESTPlugin (auto-generated API)
425430
├── DispatcherPlugin
426-
├── AIServicePlugin (if available)
427-
└── StudioPlugin (if --ui flag)
431+
└── AIServicePlugin (if available)
428432
5. Runtime.start() → init + start all plugins
429433
6. Server listens on the resolved port (see "Ports & networking" in Part 3)
430434
```
@@ -958,6 +962,10 @@ describe('AuditPlugin', () => {
958962
| `com.objectstack.metadata` | `metadata` | `@objectstack/metadata` |
959963
| `com.objectstack.realtime` | `realtime` | `@objectstack/service-realtime` |
960964
| `com.objectstack.cache` | `cache` | `@objectstack/service-cache` |
965+
| `com.objectstack.setup` || `@objectstack/setup``createSetupAppPlugin` (ADR-0048 one-app pkg) |
966+
| `com.objectstack.studio` || `@objectstack/studio``createStudioAppPlugin` |
967+
| `com.objectstack.account` || `@objectstack/account``createAccountAppPlugin` |
968+
| `com.objectstack.cloud-connection` || `@objectstack/cloud-connection``createCloudConnectionPlugin` |
961969

962970
---
963971

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

969977
- Do **not** register `sys_metadata` or `sys_metadata_history` from an ObjectOS
970978
runtime plugin. Those persistence tables belong to the control plane.
979+
(Exception, #1826: an *isolated project kernel* may opt into `sys_metadata`
980+
hydration from its own DB — the general boundary otherwise stands.)
971981
- Do **not** call `MetadataManager.setDataEngine()` automatically from
972982
`MetadataPlugin.start()`. Project databases must contain business rows only.
973983
- Use `artifactSource: { mode: 'local-file', path: './dist/objectstack.json' }`
@@ -1124,6 +1134,17 @@ Port resolution is the same for `os dev` and `os start` (both spawn `os serve`):
11241134
| `os publish` | Push the compiled stack to a cloud environment |
11251135
| `os register` | Register the local stack as a deployable target |
11261136
| `os cloud …` | Cloud-specific subcommands (logs, metrics, status) |
1137+
| `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) |
1138+
| `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 |
1139+
1140+
> **Cloud connection & marketplace (`@objectstack/cloud-connection`, ADR-0008/0009).**
1141+
> The open runtime-side cloud client. Its plugins —
1142+
> `CloudConnectionPlugin`/`createCloudConnectionPlugin`, `MarketplaceProxyPlugin`,
1143+
> `MarketplaceInstallLocalPlugin`, `RuntimeConfigPlugin` — expose the install-local
1144+
> endpoint that `os package install` targets, ship the **Installed Apps** page and
1145+
> marketplace Setup nav as plugin metadata, and maintain `LocalManifestSource`
1146+
> (a local desired-state ledger) plus runtime-identity bind v2 (environment-less
1147+
> self-hosted binding).
11271148
11281149
## Testing pattern
11291150

skills/objectstack-ui/SKILL.md

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

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

@@ -1096,6 +1106,29 @@ export const AddToCampaignAction: Action = {
10961106
};
10971107
```
10981108

1109+
### Opening in a New Tab (`opensInNewTab` / `newTabUrl`)
1110+
1111+
For actions that should land in a new browser tab, set `opensInNewTab: true`
1112+
(#1787). The renderer pre-opens the tab **synchronously** on click so popup
1113+
blockers don't fire, then navigates it to the handler's returned `redirectUrl`.
1114+
1115+
For external deep-links / SSO with no server round-trip, add `newTabUrl` — a
1116+
direct URL template (supports the `{recordId}` placeholder). It is valid **only**
1117+
alongside `opensInNewTab: true`, and the target endpoint must enforce its own
1118+
auth (the new tab carries no in-app session context).
1119+
1120+
```typescript
1121+
export const OpenInvoicePdfAction: Action = {
1122+
name: 'open_invoice_pdf',
1123+
label: 'Open PDF',
1124+
objectName: 'invoice',
1125+
type: 'url',
1126+
opensInNewTab: true,
1127+
newTabUrl: '/api/v1/invoice/{recordId}/pdf', // zero-roundtrip; endpoint self-auths
1128+
locations: ['record_header'],
1129+
};
1130+
```
1131+
10991132
### Action Parameter Patterns
11001133

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

0 commit comments

Comments
 (0)