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
12 changes: 12 additions & 0 deletions .changeset/28577-adr-0146-skills-docs-teaching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/skills': patch
---

docs(skills): the ADR-0126 §1.3 promise pages and the skills teach what ADR-0146 D2 delivers — customizations in `.custom`, `__c` fields, the flow override and "new similar", no package-less overlay

Clause-②: no
Metadata-protocol: none

- **Before.** The skills taught the pre-epic paths: `objectstack-data` named no home for a customer's extension of a shipped object and no `__c` rule, and its lock table still called an edit of a shipped item an "overlay"; `objectstack-automation` said nothing about a flow an installed package ships, so an agent cloned it (two copies running) or edited it in place (refused); `objectstack-platform` taught the declared override's read but not who may declare it, how the pair is created, or what the three non-overridable types do. The docs promise pages (`capabilities/integrations`, `build-without-code`) still said views and dashboards are "yours to edit directly" and sent readers to a Setup packaged-automation page to switch off and clone a flow.
- **Now.** `objectstack-data` says an object an installed package ships is extended from the base's paired `.custom` package, every added field ends in `__c`, the suffix is refused anywhere else and an unsuffixed field is refused in the pair, a base field changes only through `fieldPatches`, and Studio's Add field writes the same extension; its lock table reads "Edit", not "Edit (overlay)". `objectstack-automation` says a packaged flow is customized by a same-named declared override in the pair (armed in the base's place, one activation switch, a suspended run finishes on the body it started on), that an undeclared same name is refused at publish, and that the clone door is "new similar": a copy under a new name, switched off, in the pair, never package-less. `objectstack-platform` adds the ensure door and its two manifest facts, the customizations read, the uninstall refusal, the write gate's one rule for dependents (rename), and "new similar" for permission sets, positions and capabilities. One eval per skill asserts the taught shape and refuses the old one.
- Every file stays under its token ceiling: the new text is paid for by deleting restatement in the same file (a layout tree the Template section and the Naming table already state; a search-mirror maintenance table and a formula aside restating the sentence above them; the dates and money legacy → CEL rows `objectstack-formula`'s table holds; the CEL callout restating that skill's pointer; two `runAs` notes and a `readonly` note tightened, no rule removed). The touched ceilings lower to their new measurements; no ceiling rises, no frontmatter, schema or behaviour changes. The docs site's promise pages, install modes, object extensions, metadata lifecycle, permission sets, CLI and metadata API pages gain the same facts.
15 changes: 15 additions & 0 deletions content/docs/api/metadata-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ unaffected. See [unrecognised type spelling](/docs/api/error-catalog#invalid_req
in the error catalog for the envelope — the same code and status cover both this
refusal and the misspelling one above; the message text tells them apart.

**An item an installed package ships is customized in that package's pair**
([ADR-0146](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md)
D2, D11). A `PUT` naming no package onto an item one installed read-only package
`hotcrm` ships is refused `403 NOT_OVERRIDABLE`; for a type an override applies to,
the sentence names the route: save it with `?package=hotcrm.custom`, the body
declaring `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` (`basedOn` is the
`manifest.version` of `hotcrm` it was taken from, recorded and never judged). Only the pair declares an override: an item of any other
package that depends on `hotcrm` and bears a `hotcrm` item's name is refused at save
and publish, and its fix is a new name. A permission set, position or capability is
never overridden; it is copied under a new name. What the reads then serve is in
[Metadata lifecycle](/docs/concepts/metadata-lifecycle#customizing-an-installed-package-the-paired-custom-override-adr-0146).
`POST /packages/hotcrm/ensure-customization` creates the pair when absent (Studio calls
it on the first change), and `GET /packages/hotcrm/customizations` lists its overrides,
extensions and switched-off items in one read.

**Body**: `{ type: "object", name: "account", item: { ... } }`
**Response**: `{ success: true }`

Expand Down
2 changes: 1 addition & 1 deletion content/docs/build-without-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Two properties make Studio safe to hand to non-developers:

## Setup — where the platform is administered

The **System Settings** app covers platform administration: user management and invitations, permission assignment, datasources (including federated external databases), diagnostics and audit logs, and the **Marketplace** — template apps such as HotCRM install with one click and appear alongside your own apps. What "customize" means depends on the piece: views and dashboards are yours to edit directly; a packaged object grows through your own fields via an extension package, never by reshaping what shipped; a packaged flow stays locked, but a dedicated **packaged automation** page here lets you switch it off and clone your own — the clone is an ordinary flow, yours to edit in Studio.
The **System Settings** app covers platform administration: user management and invitations, permission assignment, datasources (including federated external databases), diagnostics and audit logs, and the **Marketplace** — template apps such as HotCRM install with one click and appear alongside your own apps. Setup holds no metadata editor: its metadata pages open Studio, and an installed app is customized there. Open the app in Studio and every change lands in its paired customization package (`hotcrm.custom` for HotCRM), created on your first change and exported and deployed like any package, while what shipped stays the vendor's. **Customize** on a view, dashboard, report or flow saves your version as an override of the shipped one, marked with the app it overrides, and **Restore package original** removes it. **Add field** on a shipped object adds your own field, named with a `__c` suffix, and a shipped field's label, description, help text, placeholder and option labels can be changed, nothing else. A shipped flow's switch turns it off for this deployment, and **New similar** copies a flow or a permission set under a new name; a flow's copy starts switched off.

## When to reach for code instead

Expand Down
2 changes: 1 addition & 1 deletion content/docs/capabilities/integrations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A business system earns its keep by fitting into the systems — and habits —
- **Triggers in** — external systems start platform [flows](/docs/capabilities/automation).
- **Connectors** — ready-made Slack and generic REST connectors drop into flows; an HTTP API, OpenAPI document, or MCP server can be [declared as metadata](/docs/automation/connectors) and goes live at boot, no plugin code.
- **Federated external databases** — point at an existing database and query it in place: no migration, visible in views, usable in flows.
- **Marketplace templates** — install complete apps (HotCRM among them) with one click: objects, views, flows, dashboards, and seed data included. Views and dashboards are yours to edit directly; a packaged flow stays locked, but [Setup](/docs/build-without-code) lets you switch it off and clone your own to edit in Studio; a packaged object grows through your own fields via an extension package, not by editing what shipped.
- **Marketplace templates** — install complete apps (HotCRM among them) with one click: objects, views, flows, dashboards, and seed data included. What shipped stays the vendor's: you customize an installed app in [Studio](/docs/build-without-code), and every change lands in the app's own customization package (`hotcrm.custom` for HotCRM), which exports and deploys to another environment like any package. A view, dashboard, report or flow is customized by overriding it there; a shipped object gains your own fields, named with a `__c` suffix; a shipped flow can be switched off for your deployment, or copied under a new name with **New similar**.
- **Deploy anywhere** — cloud or fully self-hosted; your data and your app definition stay yours.

## Everyday collaboration
Expand Down
8 changes: 5 additions & 3 deletions content/docs/concepts/metadata-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,15 @@ document **whole** for every read that serves the base's name

> **ADR-0131 D6 — the per-organization overlay axis is retired.** The `/meta` doors (REST and the runtime dispatcher) carry no organization into a metadata write or read: a Studio or `PUT /api/v1/meta/...` write of the five ✅ types below lands **environment-wide** (since ADR-0131 D7 the `sys_metadata` family declares no organization column) and is served to every organization, and an organization admin holding only the retired `manage_org_presentation` capability is refused (`403`). The protocol itself refuses every organization-scoped write, and every read — item, list, layers, cached, history, diff, audit, drafts and the commit timeline — is environment → code, whatever organization a caller names. An overlay row an earlier release stored under an organization — under the `single` posture, every Studio save of these types, filed under the Default Organization — is served by no read, and on a SQL database (SQLite, PostgreSQL, MySQL) still holding one a v18 `os serve` refuses to start until the v18 migration ceremony (ADR-0131 C7: `os migrate organization-ownership --out PLAN`, then `--apply --plan-file PLAN`) has carried such a row to the environment layer and dropped the column; nothing deletes or rewrites it before then. The anonymous form doors read the environment alone too: the ceremony carries every organization-layer public-form withdrawal onto the environment, fail-closed. The table still records which types take an overlay of a shipped item.

> **ADR-0146 D11 — the tier no longer opens a write door onto a packaged item.** A write naming no package — a save, a publish, a reset (`DELETE`) or a rollback — onto a ✅-type item that one installed read-only package ships is refused `403 NOT_OVERRIDABLE`, and the sentence names the base's pair: save it into `?package=<base>.custom`, declaring `overrides` ([above](#customizing-an-installed-package-the-paired-custom-override-adr-0146)). A package-less row stored before the close is still served until `os migrate packageless-overlays` moves it into the pair as a declared override; the ✅ row is now the set that ceremony moves and the read path honours for an unmoved row. A package-less write of a name no package ships is environment metadata, and stays open.

| Type | `allowEnvironmentOverlay` | Rationale |
| :--- | :---: | :--- |
| `view`, `dashboard`, `report`, `email_template`, `translation` | ✅ | Pure rendering / render-time content. Customizing it in an environment is safe. |
| `flow` | ❌ | Flows carry execution side-effects (events, jobs, audit), and the `flow` entry declares `supportsOverlay: false` — the loader cannot merge a per-org flow overlay, so the write permission granted a write nothing could read back: an org-scoped flow overlay wrote successfully and **lost its binding on the next cold start**. Rolled back (#6283) so the silent phantom is a loud `403 not_overridable` at the moment of the write. **`allowRuntimeCreate` stays `true`** — a tenant may still author a *brand-new* flow through the runtime API (ADR-0070 package-first authoring); what is closed is overlaying a *packaged* flow. |
| `agent` | ❌ | Agents are platform-owned and closed to third parties (ADR-0063 §2) — no environment fork of a packaged agent. |
| `permission`, `position` | ❌ | Authorization correctness — an overlay of a packaged permission set *is* silent privilege drift (ADR-0005's security row). Rolled back from an unratified `true` by the 2026-08-08 maintainer ruling (#6483); `position` takes the amendment's `false` default for new types. **`allowRuntimeCreate` stays `true`**: runtime-created sets (including package-bound rows materialized through the metadata door) keep working, while an admin-door edit of a *code-declared* set refuses with `403 not_overridable`, naming the sanctioned path: clone the set under a new name and edit the clone ([ADR-0126](#packaged-flows-actions-and-permission-sets-adr-0126)). The package's own author still changes the declaration in the package and re-publishes (ADR-0086 two-doors). |
| `object` | ❌ | Defines the table schema. Overriding a packaged object would break existing data — but `allowRuntimeCreate: true`, so tenants *can* author brand-new objects, **and this is also how a new field is added** (write the object with the field in `fields`). |
| `object` | ❌ | Defines the table schema. Overriding a packaged object would break existing data — but `allowRuntimeCreate: true`, so tenants *can* author brand-new objects, **and this is also how a new field is added** (write the object with the field in `fields`). On an object an installed package ships, a field is added as an object extension in the base's paired `.custom` package, named `<name>__c` ([Object extensions](/docs/data-modeling/object-extensions#customizing-an-installed-packages-object)). |
| `field` | ❌ | **Also `allowRuntimeCreate: false` since protocol 17** (#7893). A field is not a standalone artifact — fields are authored inside their object (`ObjectSchema.fields`), so a `field` write minted a *separate* `sys_metadata` row keyed `('field','<object>.<name>')` that nothing ever composed into the parent: `PUT /meta/field/showcase_task.zz_probe` answered 200 `state=active`, the row read back `_diagnostics.valid: true`, and `GET /meta/object/showcase_task` never listed the field. The door is closed rather than bridged: add a field by writing its **object** (`PUT /meta/object/:name`, or `**/*.object.ts` and redeploy), which both persists and composes. Existing rows are untouched — they were inert before the change too — and remain deletable. |
| `datasource` | ❌ | Connection strings; multi-tenant isolation is enforced at a higher layer. (`allowRuntimeCreate: true` — the datasource wizard persists `origin: 'runtime'` rows.) |
| `job` | ❌ | **Also `allowRuntimeCreate: false` since protocol 17** (#4509). `JobSchema.handler` names a function in the compiled bundle's function table, which a runtime writer has no way to reach — so a job created in Studio or through `PUT /meta` parsed, saved, reported success and was never scheduled. The door is closed rather than bridged: `job` stays first-class through `*.job.ts` / `defineStack({ jobs, functions })`, where every schedule shape, `retryPolicy` and `timeout` does reach the scheduler. Existing rows are untouched — they were never scheduled — and `migrateStoredMetadata` reports them `skipped`. |
Expand All @@ -184,7 +186,7 @@ There is no `workflow` metadata type (per [ADR-0020](https://github.com/objectst

The gate is **two-tier** — `allowEnvironmentOverlay: false` is not the same as "no runtime writes":

- Overwriting an item that ships from a code package requires `allowEnvironmentOverlay`. Denied → `403 not_overridable`.
- Overwriting an item that ships from a code package is refused → `403 not_overridable`. The change is a declared override saved into the base's paired `.custom` package ([above](#customizing-an-installed-package-the-paired-custom-override-adr-0146)); for a ✅ type, a package-less row stored before ADR-0146 D11 is still served until it is moved.
- Creating a brand-new item that has no artifact backing requires `allowRuntimeCreate` (or `allowEnvironmentOverlay`). Denied → `403 not_creatable`.

See [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0005-metadata-customization-overlay.md) for the full design and amendments.
Expand All @@ -195,7 +197,7 @@ See [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/

| Type | Sanctioned path the refusal names |
| :--- | :--- |
| `flow` | Clone it under a new name (`POST /api/v1/automation/:name/clone`, body `{name, label}`), or switch it off (`POST /api/v1/automation/:name/toggle`, body `{enabled: false}`). A flow that holds a credential (an inbound hook's `secret`, an `http` node's `signingSecret`) is not cloned in one step: the clone door refuses it with `409`, because a copy would share the secret, and the copy is authored as a new flow with its own. The copy is written switched off, into the source package's paired customization package `<base>.custom` (created on first use), or into the writable package the body's optional `package` names; a flow no read-only package ships has no pair, so its clone names `package`. The answer's `packageId` says where the copy landed ([ADR-0146](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md) D3, D4). |
| `flow` | Declare an override from the base's paired `<base>.custom`, or rename it (a write from any other package is told to rename: only the pair declares an override), or switch it off (`POST /api/v1/automation/:name/toggle`, body `{enabled: false}`). The declared override is armed in the base flow's place, keeps its one activation switch, and a run suspended on the base finishes on the version it started on (ADR-0146 D9). A copy under a new name is "New similar", the clone door `POST /api/v1/automation/:name/clone` (body `{name, label}`), which the removal refusal still names. A flow that holds a credential (an inbound hook's `secret`, an `http` node's `signingSecret`) is not cloned in one step: the clone door refuses it with `409`, because a copy would share the secret, and the copy is authored as a new flow with its own. The copy is written switched off, into the source package's paired customization package `<base>.custom` (created on first use), or into the writable package the body's optional `package` names; a flow no read-only package ships has no pair, so its clone names `package`. The answer's `packageId` says where the copy landed ([ADR-0146](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0146-studio-designs-setup-administers-one-editor-per-item-paired-customize-package.md) D3, D4). |
| `action` | Switch it off (`POST /api/v1/actions/_activation/:object/:action`, body `{enabled: false}`; `:object` is `global` for an object-less action). No clone is offered. |
| `permission` | Clone it under a new name: the "Clone" action on the permission set, or the clone door `POST /api/v1/security/permission-sets/:name/clone` (body `{name, label, package?}`) with a new name. The clone door copies the whole definition from the security catalog; a copy of a packaged set lands in the package's paired `<X>.custom` (ADR-0146 D3/D6). |

Expand Down
Loading
Loading