Skip to content

Config-JSONB realignment backlog

Status: Current | Last reviewed: 2026-06-23

A prioritized record of agent_versions.config / workflow_versions.config JSONB inconsistencies that were audited during the 2026-06-23 config-shape pass but not (or only partially) executed. Read this before touching config-shape internals so you don't re-discover a deferred item or re-litigate a deliberate decision.

This is the config-internals sibling of cleanup-proposals.md — that page tracks stale tables/columns; this one tracks the shapes inside the two config JSONB blobs. When you act on an open item, move it to "Resolved" with the migration number.

The 2026-06-23 pass shipped 005_normalize_config_shapes.sql plus matching runtime/editor changes. What it converged is under Resolved; what it deliberately left is under Open.

Open backlog

Each item: Issue / Blast radius / Recommendation / Why deferred.

1. Migration-003 gap: orphaned top-level config.injection / config.ivr

Issue 003_migrate_node_builtin_configs.sql moved Operations capability config (injection, ivr, ces, warm_transfer, mcp_url_catalogs) off the top level and onto the owning pipeline node's builtin_tool_configs, then stripped the top-level keys. The audit observed ~6 agent_versions still carrying a top-level config.injection (and ~3 a top-level config.ivr) with no matching node-level builtin_tool_configs.message_injection. These rows were saved after 003 ran, so 003 never touched them. The runtime no longer reads the top-level copy — injection/ivr is silently un-wired on those agents.
Blast radius Small (a handful of agents) but a correctness bug on each: injection/ivr looks configured in the row but does nothing at runtime. Verify the exact row count before acting.
Recommendation A follow-up forward migration that re-runs the 003 node-promotion for any remaining top-level injection/ivr/ces/warm_transfer keys, or simply re-save each affected agent through the editor (which writes the node-level shape).
Why deferred This is a 003-completion task (rows that slipped in after the one-time migration), not part of the config-shape normalization that 004 owned.

2. Orphaned Injection / Ivr $defs in the agent schema

Issue agent-config.schema.json still defines $defs.Injection and $defs.Ivr, but nothing $refs them anymore (confirmed: zero references) — injection/ivr config now lives inline under the node builtin_tool_configs objects (per item 1's migration). Dead weight.
Blast radius None. ajv ignores unreferenced $defs; validation is unaffected.
Recommendation Either $ref them from the builtin_tool_configs inline objects (so the shapes are validated again) or delete them.
Why deferred Cosmetic; harmless. Low priority.

3. camelCase vs snake_case key mix

Issue Top-level config keys are snake_case (execution_mode, output_bindings, target_language), but some stage/editor-owned fields are camelCase — notably canvas/UI fields such as position. The two conventions coexist in one blob.
Blast radius High. Renaming touches the editor serializer (flowElementsToPipeline), the collab Y.Doc shape converters (pipeline-shape.ts), the TS types, and every reader — and the collab allow-list silently drops any key not threaded through both directions.
Recommendation Document as a known, accepted inconsistency. Do not converge opportunistically.
Why deferred Cosmetic with high blast radius and no runtime payoff.

4. config.tools[] is an undiscriminated string union

Issue config.tools is string[] (see pipeline-types.ts L872) whose entries are one of three forms with no discriminator key: a bare tool-config uuid, builtin:<id>, or inline:<id>. Readers disambiguate by string prefix.
Blast radius Large. A discriminated object form ({kind, id}) would touch the editor, the collab shape, the orchestrator wiring, and every persisted config.
Recommendation Document the prefix convention (already enforced in code — bare = uuid, builtin: = built-in capability, inline: = config.inline_tools[] entry). Leave the wire shape as a string union.
Why deferred The prefix convention works and is enforced; an object form is a large migration for stylistic gain.

5. output_parameters vs response_variables on workflow config

Issue Near-aliases on WorkflowConfig. async-agent reads output_parameters or response_variables (one falls back to the other — see workflow_tool.py L145-L146); consumer.py L555 reads output_parameters only.
Blast radius Low. Both are optional and additive.
Recommendation Keep both — they carry distinct documented intent (workflow-types.ts L696-L705): output_parameters = outputs surfaced to a generic workflow caller; response_variables = outputs consumed by a conversation lifecycle hook (become {{lifecycle.<name>}} merge fields). Document the precedence (output_parameters wins where both readers run).
Why deferred Not actually a duplicate; collapsing them would lose the documented distinction. Low priority.

6. multimodality.catalog_id (singular) vs catalogs[]

Issue Node config.multimodality.catalog_id is a deprecated singular field; the canonical field is catalogs?: string[] (a multi-select of catalog URIs). The type is explicitly marked @deprecated (pipeline-types.ts L1154-L1165) and kept only so legacy rows read without crashing.
Blast radius Low. Readers already prefer catalogs.
Recommendation Treat catalog_id as read-only legacy; never write it. A future migration could fold any surviving catalog_id into catalogs[] and drop the field.
Why deferred Already deprecated and tolerated; not worth a migration alone.

7. TS ⇄ Python runtime field-name divergence (verify per field)

Issue The TS editor types and the Python node handlers are independent surfaces, so a field can drift. The audit flagged a few; on inspection they are dual-shape back-compat, not bugs — but they confirm the divergence risk: foreach reads two distinct body representations (body = legacy list of sibling node ids run sequentially; body_graph = {nodes, edges} inline sub-graph for nested foreach) — not aliases, both live (foreach.py L95-L109). assignment bindings read value (canonical) with a source legacy fallback and an operator of set\|append\|prepend\|concat\|increment (assignment.py L63-L70).
Blast radius Varies per field; a true mismatch silently drops config on one side.
Recommendation Per field, confirm the authoritative side (Python handler is the runtime source of truth) before changing either. Audit observed no concrete typo-path mismatch in foreach/assignment — the apparent ones are deliberate legacy fallbacks. Treat any new finding as "verify against the node handler, then fix the non-authoritative side."
Why deferred No confirmed bug; this is a standing risk to check, not a scheduled change.

Resolved

Shipped 2026-06-23 by 005_normalize_config_shapes.sql (idempotent; folded into migrate-city-db.sh) plus the runtime/editor changes noted. After 004, the schemas under shared/service-definitions/schemas/ validate every config with zero errors.

Area What converged
Dead-data cleanup Dropped config.settings.lifecycle (0 rows used it; canonical is top-level config.lifecycle). Normalized execution_mode: 'scheduled' → 'async' (unhandled value; scheduling rides trigger.kind='schedule'). Stripped deprecated top-level config.name / config.description / config.extension mirrors (authoritative on the agents row / stage config). Coerced code-node output_bindings: [] → {} (canonical object shape).
ocr_extract Legacy input{source,value} + integration_id + features + queries + output_schema → canonical document_source / document_reference / ocr_integration_id / aws_features / aws_queries / llm_output_schema / processing_type. The runtime _normalise_legacy_config shim was removed from ocr_extract.py.
router Legacy condition_field / routes → canonical config.outputs=[{id,label,expression}], with output ids derived from the outgoing edge condition values (edges are the routing source of truth). The runtime _normalise_config legacy branch in router.py and the config.routes fallback in workflow_graph_builder.py were removed. This also fixed a latent bug: in the squashed runtime a condition_field-only router produced empty outputs and always fell through to __default__ (it never branched).
translation Flow-level source_language / target_language / voice_config collapsed down into the owning stages (STT language, translation/S2ST stage source_language/target_language, TTS voice_id). The orchestrator translation_builder.py now reads stage-first; the editor (TranslationPipelineEditor.tsx, default-configs.ts), schema.ts validation, and pipeline-types.ts were moved to stage-level. Flow-level fields stay optional in the schema as a fallback for not-yet-migrated rows but are deprecated.
lifecycle dual-home universal_builder.py no longer reads config.settings.lifecycle as an or fallback — only top-level config.lifecycle.

See also