SKILL.md7.9 KBView on GitHub ---
name: update-org-aop
description: >
Safely update org-level AOP configurations (org_aops table). Use when the user wants to change
an org AOP's text, add/modify custom fields, change status options, or propagate changes to all
team members. Covers what to change, what never to touch, how to check current state, and when
to use force vs soft propagation.
---
# Update Org AOP
The `org_aops` table is the canonical schema source of truth for all AOP types across an org.
Changes to `org_aops` propagate to all linked user AOPs via the fan-out service.
## Architecture reminder
```
org_aops (source of truth)
↓ orgAopId FK
agent_operating_procedures (per user, full materialized copy)
user_diff: { sections: {...}, customFieldDefinitions: {...} } ← what the user has changed
```
Soft propagation (default): org changes are merged into user AOPs, skipping dimensions present in `user_diff`.
Force propagation (change-scoped): only the dimensions that actually changed in the save are force-pushed. Users' customisations on untouched sections/fields survive even a force push — force is not a blanket overwrite.
Both modes only fire for AOPs where content actually changed. Saving all AOPs at once only propagates the ones with real diffs.
---
## Before making any change — check current state
Always run these inspection queries before editing:
```sql
-- 1. See all org_aops for an org
SELECT oa.id, oa.name,
CASE WHEN oa.agent_operating_procedure IS NOT NULL THEN 'yes' ELSE 'NO' END AS has_aop,
CASE WHEN oa.custom_field_definitions IS NOT NULL THEN 'yes' ELSE 'NO' END AS has_cfd
FROM org_aops oa
JOIN organizations o ON o.id = oa.organization_id
WHERE o.name = '<OrgName>'
ORDER BY oa.name;
-- 2. Count linked user AOPs and how many have diverged for a given org_aop
SELECT aop.user_id, u.email,
CASE WHEN aop.user_diff = '{}'::jsonb THEN 'in sync' ELSE 'differs' END AS sync_status,
(SELECT string_agg(k, ', ') FROM jsonb_object_keys(aop.user_diff) k) AS diverged_keys
FROM agent_operating_procedures aop
JOIN "user" u ON u.id = aop.user_id
WHERE aop.org_aop_id = '<org_aop_id>';
-- 3. See a specific org_aop's custom field definitions
SELECT id, name, custom_field_definitions
FROM org_aops
WHERE id = '<org_aop_id>';
```
---
## What you CAN change
| Dimension | How to change | Propagates? |
|---|---|---|
| AOP text sections (style_guide, filtering_rules, etc.) | Edit via playground org mode or `/cedarAdmin` | Yes — soft by default |
| `company_sop` section | Edit via playground or `/cedarAdmin` | Always **force** (no user overrides allowed) |
| `selection_procedure` | Edit via playground or `/cedarAdmin` | Yes — soft by default |
| `custom_field_definitions` — add new field | Add entry with new unique ID, never reuse IDs | Yes |
| `custom_field_definitions` — change field label/description/instructions | Update those properties | Yes |
| `custom_field_definitions` — change `agentEditsForbidden` | Update the property | Yes |
| `custom_field_definitions` — change `displayScope` or `displayOrder` | Update the property | Yes |
| `conversation_field_definitions` (status/priority options) | Edit options | Yes, org-only (no user override) |
| `conversation_overview_configuration` | Edit card layout | Yes — soft by default, users can override |
| `color` | Update the color | Yes |
| `is_no_op` | Toggle true/false | Yes |
---
## What you MUST NEVER change
| Thing | Why |
|---|---|
| A field's `id` | It is the join key across ALL `crm_conversation_field_values` rows. Changing it orphans all existing data. **Server enforced — write will be rejected.** |
| A field's `type` | Changing type corrupts existing stored values (e.g., date field storing "strong"). **Server enforced — write will be rejected.** |
| A field's `label` | Semantic identifier shown to users. Renaming it silently redefines what the field means without migrating data. Users can override label locally via `user_diff`. **Server enforced — write will be rejected.** |
| An org_aop's `id` | FKs from `agent_operating_procedures.org_aop_id` break. |
| An org_aop's `name` after it has linked user AOPs | The matching logic for new users uses normalized name. |
---
## How to trigger propagation
**Via the UI (preferred):**
1. Open the playground AOP editor (`/playground/aop-editor`) in org mode
2. Select the org and AOP type
3. Make changes
4. Toggle "Force push to all users" ON/OFF as needed
5. Save
**Via SQL (emergency only, requires approval):**
```sql
-- After editing org_aops directly, trigger propagation through one of the tRPC routes below.
-- There is no standalone backfill script: the user_diff recompute it used to call
-- (services/org/aop-utils) was retired, so the script could not import and was removed.
```
**Via tRPC (programmatic):**
- `trpc.cedarAdmin.updateOrganizationDefaultAops({ organizationId, defaultAopConfigs, forcePropagate })`
- `trpc.orgAdmin.updateDefaultAops({ defaultAopConfigs, forcePropagate })`
---
## Adding a new custom field
1. Generate a stable UUID for the field ID (e.g., `cf_<descriptive_name>_<short_uuid>`)
2. Decide: `type` (text, number, select, date, boolean, currency, url, email, phone, **list**)
- `list` = extraction field, accumulates one row per event (formerly `_ii_` prefix fields)
- All others = working memory, one live value per conversation (UPSERT semantics)
3. Set `displayScope`: `'user'` (shows in per-deal card) or `'org'` (org analytics only)
4. Add to `org_aops.custom_field_definitions` as a new key
5. Save via playground → propagates to all linked user AOPs (soft)
6. New field will appear in all users' conversations on next agent run
Example field structure:
```json
{
"cf_champion_b3a1": {
"id": "cf_champion_b3a1",
"type": "text",
"label": "Champion",
"description": "The internal champion at the prospect company.",
"displayOrder": 5,
"displayScope": "user",
"agentEditsForbidden": false
}
}
```
---
## Changing `company_sop` (should always force propagate)
`company_sop` carries compliance and branding rules that should apply to everyone.
The code does not automatically enforce this — you must toggle force push manually.
When you change it:
1. Make the change in the playground org mode
2. **Toggle "Force push to all users" ON before saving**
3. This force-pushes the `company_sop` section specifically (other sections users have customised are still preserved — force is change-scoped)
---
## When to use force propagation
Remember: force is **change-scoped** — it only applies to the specific dimensions you changed. Users' customisations on untouched sections/fields always survive.
Use force when:
- You changed `company_sop` (compliance/branding — should apply to everyone, even those who customised it)
- You removed a field that users might have overridden in their `user_diff`
- You restructured field options and users' personal versions are now invalid
- The org admin explicitly wants to override everyone's version of a specific thing
Do NOT force when:
- You added a new field (soft is fine — users inherit it automatically)
- You changed instructions or descriptions (users who personalised these should keep their versions)
- You changed the AOP text sections other than `company_sop`
---
## Verifying propagation worked
After saving, run this to confirm user AOPs are updated:
```sql
SELECT u.email,
CASE WHEN aop.user_diff = '{}'::jsonb THEN 'in sync' ELSE 'differs' END AS status,
(SELECT string_agg(k, ', ') FROM jsonb_object_keys(aop.user_diff) k) AS remaining_diffs
FROM agent_operating_procedures aop
JOIN "user" u ON u.id = aop.user_id
WHERE aop.org_aop_id = '<org_aop_id>'
ORDER BY u.email;
```
After a force push, users who had customised the changed section/field will show `'in sync'` for those keys. Users who had customised OTHER dimensions (ones the admin didn't touch) will still show `'differs'` for those keys — this is expected and correct with change-scoped force.