# Action Table Bridge

Date: 2026-05-16

## Contract

There is no implicit row-to-row bridge between `_System_Action` and `_System_StandardAction`.

The two tables use the word "action" for different runtime surfaces:

- `_System_Action`: object-scoped quick/custom/automation action metadata.
- `_System_StandardAction`: global invocable standard-action registry for `/actions/standard/:name`.

They are disjoint namespaces. `_System_Action` is keyed by `(ObjectApiName, Name)`;
`_System_StandardAction` is keyed by `Name`. A `Name` that happens to appear in both
tables is **not** a conflict and **not** a bridge — no runtime path joins the two by
name, so name equality across them is never treated as corruption.

## Authorities

`_System_Action`

- SSoT for object placement and generic action execution metadata.
- Business key: `(ObjectApiName, Name)`.
- Runtime readers: metadata service, quick action handler, automation action service.
- Pack writer: platform/source-backed UX action payloads.

`_System_StandardAction`

- SSoT for invocable standard-action route metadata.
- Route key: `Name`.
- Parent key: `Id`.
- Runtime readers: `StandardActionDispatcher`, standard-action list endpoint, flow invocation access scoping.
- Pack writer: invocable standard-action payloads.

Standard-action satellites

- `_System_ActionSchema.StandardActionId`
- `_System_ActionFieldDefault.StandardActionId`
- `_System_ActionInputGuard.StandardActionId`
- `_System_StandardActionInternalAccessScope.StandardActionId`

These columns are the supported bridge. They must reference a **present**
(non-deleted) `_System_StandardAction.Id`.

## Lifecycle Invariant: satellites never outlive their parent

A satellite row must not stay live once its `_System_StandardAction` parent is
soft-deleted (the parent is dropped when a pack stops shipping the action).
Two halves keep this true:

- **Write side (reconcile cascade).** `InitializeStandardActionsForOwner` runs
  `cascadeDeactivateOrphanedStandardActionSatellites` after the parent reconcile.
  It soft-deletes (`IsActive=0, IsDeleted=1`) any satellite row whose
  `StandardActionId` no longer resolves to a present (non-deleted) parent.
- **Read side (verify gate).** `controlplane.VerifyActionTableConsistency`
  (deep `cmd/setup verify`) fails closed if any such orphan still exists.

**Presence, not visibility.** Liveness keys on `IsDeleted`, not `IsActive`.
`_System_StandardAction.IsActive=0` is an admin disable-without-delete toggle
(preserved across pack updates), so a disabled-but-present parent still owns its
satellites — neither the cascade nor the gate touches them. Only a deleted
(`IsDeleted=1`) or missing parent makes its satellites orphans; reconcile sets
`IsActive=0, IsDeleted=1` together when it drops a parent.

**Customization scope.** The three customizable satellites
(`_System_ActionSchema`, `_System_ActionFieldDefault`, `_System_ActionInputGuard`)
preserve admin-customized rows: an `IsCustomized=1` orphan is a supported admin
state, so both the cascade and the gate are scoped to `IsCustomized=0` and leave
it alone. `_System_StandardActionInternalAccessScope` is L1-only (no admin
author), so every orphan is swept and checked regardless of `IsCustomized`.

This is why the gate does **not** check for `_System_Action` ↔
`_System_StandardAction` name collisions: that pairing is not a real bridge (see
Contract above), and failing on it would block valid admin-authored QuickAction
/ automation metadata with no apply-side repair path.

## Legacy Standard Actions

`changeOwner` and `createRecord` still fall through to the legacy standard action handler when there is no `_System_StandardAction` row. That is a code migration branch, not a metadata bridge. When those actions migrate to the registry, they should become `_System_StandardAction` rows and must not be mirrored through `_System_Action` by name.

## Future Migration Rule

If a future design wants `_System_Action` and `_System_StandardAction` rows to pair directly, the pairing must be explicit. Add a column such as `StandardActionId` or a typed action kind, migrate existing rows, and update `VerifyActionTableConsistency` in the same PR so mismatches fail before runtime.
