---
apiVersion: data-spec/v1
id: clear-residual-workflow-childcopy-20260424
description: "Strip legacy workflow.childCopy from _System_Action.Config for objects that now carry object-level ChildCloneRules authority"
issue: "#1847"
created: 2026-04-24
scope: production-only
status: pending
tables: [_System_Action]
depends_on: []
---

# Clear Residual `workflow.childCopy` From `_System_Action.Config`

## Summary

PR #1853 atomic-cutover moved Quote/Order child-clone authority from
action-level `workflow.childCopy` inside `_System_Action.Config` to
object-level `_System_Object.ChildCloneRules`. The PingCAP tenant seed
(`tenants/pingcap/actions.json`) was updated to remove
`CreateQuote.workflow.childCopy`, but the tenant overlay writer
deep-merges Config rather than replacing it. On upgraded orgs, old
`workflow.childCopy` keys survive the merge and remain in DB.

## This spec is OPTIONAL hygiene — not a correctness requirement

**Phase 2 of PR #1853 retired the action-level `workflow.childCopy`
runtime executor entirely** (`standard_actions_handler.go` no longer
reads `workflow.childCopy` from Config). That means:

- **Residue is inert.** A `_System_Action.Config` row with lingering
  `workflow.childCopy` keys is harmless — no code path reads them, so
  they can't execute, so they can't duplicate child records.
- **No upgrade ordering is required.** Deploy the new binary, run
  `apply-tenant`, serve traffic. Nothing blocks on residue.
- **This data-spec is dead-config hygiene.** It removes keys that no
  longer have meaning. Running it makes `_System_Action.Config`
  metadata exports cleaner and prevents future confusion ("what's
  this `childCopy` field that nothing reads?"), but you can also
  ignore it indefinitely.

Run this spec when an operator wants tidy metadata; skip it if the
residue doesn't bother you.

Coexistence at runtime means `PersistenceService.Insert` fires the
object-level clone once (via `applyChildCloneRules`) and
`StandardActionsHandler.createSingleRecord` fires the action-level clone
again (via the `workflow.childCopy` loop), duplicating every child
record on every CreateQuote / CreateOrder call.

Commit 8a of round-7 added:
- Authoring-time mutex (rejects new coexistence at write time).
- Runtime guard (skips the action-level loop when the target object has
  object-level rules — log line "skipped N workflow.childCopy
  entrie(s)").

The runtime guard prevents the duplication, but the stale keys in
`_System_Action.Config` are still dead weight: invisible to operators,
noisy in metadata exports, and a latent hazard if the runtime guard is
ever removed. This spec clears them.

## Changes

### 0. Target set — keep in sync with the seed

The UPDATE below keys off an explicit object allowlist, NOT a JOIN
against `_System_Object.ChildCloneRules`. Why: on an upgraded org that
has not yet completed a post-cutover `apply-tenant`, the DB column
`_System_Object.ChildCloneRules` is still NULL — querying it would
match zero rows and the spec would silently no-op, leaving residual
`workflow.childCopy` in place. The next `apply-tenant` would then fail
at the coexistence guard, deadlocking the upgrade (round-7 reviewer
finding on upgrade-population mismatch).

The allowlist below MUST equal the set of top-level keys in
`backend/internal/controlplane/tenants/pingcap/object_child_clone_rules.json`.
Today that is exactly `('Quote', 'Order')`. Any future PR that extends
`object_child_clone_rules.json` with a new target object MUST also
ship a sibling data-spec that adds that object to this allowlist for
orgs upgrading across that boundary, or roll the new object into a
compacted follow-up spec.

### 1. Preflight — enumerate rows that carry residual `workflow.childCopy`

Run this first to produce the audit baseline. If count is 0, the spec is
already effectively applied — skip step 2.

```sql
SELECT
  a.Id,
  a.ObjectApiName  AS host_object,
  a.Name           AS action_name,
  a.TargetObject   AS create_target,
  JSON_LENGTH(JSON_EXTRACT(a.Config, '$.workflow.childCopy')) AS residual_count
FROM _System_Action a
WHERE a.TargetObject IN ('Quote', 'Order')
  AND JSON_LENGTH(JSON_EXTRACT(a.Config, '$.workflow.childCopy')) > 0
  AND a.IsDeleted = 0;
```

**Expected rows on a PingCAP upgraded org:** the single `CreateQuote`
action on Opportunity (target=Quote), because the pre-cutover Config
carried `workflow.childCopy`. Greenfield orgs (no pre-cutover action
Config residue) return zero rows regardless of whether
`_System_Object.ChildCloneRules` has been seeded yet.

**Idempotency**: read-only. Safe to run any number of times.

### 2. Apply — remove `workflow.childCopy` key, preserve everything else

```sql
UPDATE _System_Action a
SET a.Config = JSON_REMOVE(a.Config, '$.workflow.childCopy'),
    a.LastModifiedDate = NOW()
WHERE a.TargetObject IN ('Quote', 'Order')
  AND JSON_LENGTH(JSON_EXTRACT(a.Config, '$.workflow.childCopy')) > 0
  AND a.IsDeleted = 0;
```

**Idempotency**: the `WHERE` clause requires
`JSON_LENGTH(JSON_EXTRACT(Config, '$.workflow.childCopy')) > 0` —
rows whose `workflow.childCopy` is already absent or empty don't
match and stay untouched. Re-running after a successful apply updates
zero rows.

**Preservation**: `JSON_REMOVE` targets only the `$.workflow.childCopy`
path. Sibling keys (`fieldMappings`, `defaultValues`, `lookupField`,
`confirmTitle`, `confirmMessage`, `navigateTo`, everything else) are
byte-preserved. The surrounding `$.workflow` object stays present even
after removing its childCopy child.

**Blast radius**: bounded to actions whose `TargetObject` is `Quote`
or `Order`. No JOIN to `_System_Object.ChildCloneRules` — that state
is deliberately NOT required for this spec to work, because the spec
must run BEFORE the first post-cutover `apply-tenant` populates that
column.

### 3. Audit — confirm zero residuals remain

```sql
SELECT COUNT(*) AS residual_count
FROM _System_Action a
WHERE a.TargetObject IN ('Quote', 'Order')
  AND JSON_LENGTH(JSON_EXTRACT(a.Config, '$.workflow.childCopy')) > 0
  AND a.IsDeleted = 0;
```

**Expected after apply**: `residual_count = 0`. A non-zero value
indicates the apply step did not run to completion (partial
transaction, permissions error on `_System_Action`, or a new action
authored with coexistence between preflight and audit) — investigate
before declaring the spec applied.

## Rollback

Restoring the exact pre-apply `workflow.childCopy` bytes requires a
pre-apply snapshot of each affected row's Config. The preflight query
produces the Ids to snapshot:

```sql
-- Before apply, capture the rows identified by step 1:
CREATE TABLE IF NOT EXISTS _System_Action_childcopy_backup_1847 (
  ActionId VARCHAR(255) PRIMARY KEY,
  ConfigBefore JSON NOT NULL,
  CapturedAt DATETIME NOT NULL
);

INSERT IGNORE INTO _System_Action_childcopy_backup_1847 (ActionId, ConfigBefore, CapturedAt)
SELECT a.Id, a.Config, NOW()
FROM _System_Action a
WHERE a.TargetObject IN ('Quote', 'Order')
  AND JSON_LENGTH(JSON_EXTRACT(a.Config, '$.workflow.childCopy')) > 0
  AND a.IsDeleted = 0;
```

To roll back after apply:

```sql
UPDATE _System_Action a
JOIN _System_Action_childcopy_backup_1847 b ON b.ActionId = a.Id
SET a.Config = b.ConfigBefore,
    a.LastModifiedDate = NOW();
```

Rollback is only useful if the cutover itself is reverted — running it
with the runtime guard still active would restore dead keys that the
handler would again skip (back to the pre-spec state of invisible
residue). Rollback cleanup:

```sql
DROP TABLE IF EXISTS _System_Action_childcopy_backup_1847;
```

## Relationship to platform code

This spec is **legacy remediation**, not long-term platform logic. The
round-7 design explicitly keeps residue cleanup out of `apply-tenant`'s
hot path (reviewer decision: "升级库里残留的 workflow.childCopy 清理属
于 legacy remediation, 应该进 data-specs/, 不该做成长期 apply-tenant
special case"). Control-plane code stays generic — admin-editable
metadata and tenant-overlay metadata keep their existing contracts;
this one-shot sweep reconciles the DB to the new authority, then gets
archived.

See also:
- `backend/internal/interfaces/rest/standard_actions_handler.go`
  runtime guard (Round-7 Commit 8a) — prevents the double-execution
  that this spec's residue would otherwise cause.
- `backend/pkg/actionvalidate/coexistent_child_copy.go` authoring
  mutex — prevents NEW coexistence from being authored.
- `backend/internal/controlplane/tenants/pingcap/object_child_clone_rules.json`
  — the object-level authority that now owns these rules.
