Parse AI behavior actionParams into typed models#417
Merged
Conversation
Replace the raw dict | None action_params and its dual-alias dict-walking (aiBehaviorParams/ai_behavior_params, actionsAttributes/actions_attributes) with nested Pydantic models carrying per-declared-field camelCase aliases and extra=allow. Consumers parse once and read typed attributes; no dual-alias branches remain in SDK or MCP agent code paths. snake_case inner keys now normalize to the declared wire names, and action metadata stays a pass-through dict so GET-update round-trips serialize identically. No new validation rules. Closes #416.
This was referenced Jul 17, 2026
adriannoes
approved these changes
Jul 17, 2026
Collaborator
There was a problem hiding this comment.
Summary
Solid prep PR for typed behavior actionParams.
Nested models with per-field aliases replace the dual-alias dict walks without changing valid end-to-end wire payloads, and capability/provider hardening correctly stays out of scope for #424. Review ran checkout plus local tests (2922 passed, ruff green); CI lint and test are green.
What worked well
Per-field camelCase aliases with extra="allow" keep AutomationActionParams siblings (card_id, to_phase_id) intact while normalizing declared AI-behavior keys. Lenient BehaviorPayload reads versus strict BehaviorInput writes is a clean split, and the slug-resolve path deep-copies before parse so caller metadata is not mutated through pydantic references.
Also noted
- The public
resolve_and_populate_field_refshelper early-returns without rewriting keys when no slug fetch runs, whilepopulate_referenced_field_idsnow only reads declared camelCase names. In-repo MCP/CLI/facade paths alreadymodel_dump(by_alias=True)first, so they stay correct. If that helper remains a supported entry point for raw snake_case dicts, normalizing on every exit (or documenting dump-first) would match #416's "normalize at the entry edge" wording.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
BehaviorInput.action_paramswas a rawdict | None, and every consumer re-walked it with dual-alias branches (aiBehaviorParams/ai_behavior_params,actionsAttributes/actions_attributes): the model validator, the SDK preflight and pipe/phase-transition checks, and MCP helpers such as the pipe-ID extraction. That duplicated dict-walking contradicts the repo's "parse, don't validate" / self-guaranteeing-types rules and multiplies as more fields are added to the payload.Outcome
action_paramsis now a typed Pydantic model (AiBehaviorActionParams→AiBehaviorParams→ action and capability attributes) with per-declared-field camelCase aliases matching the GraphQL input schema andextra="allow"so unknown/sibling keys pass through verbatim. Every consumer parses once and reads typed attributes, so no dual-alias branches remain in SDK or MCP agent code paths.Behavior is unchanged for every payload that was already valid end-to-end. snake_case inner keys — which the API always rejected while the toolkit's dual-alias branches accepted them — are now normalized to the declared wire names, so that input works for the first time. Aliasing is strictly per declared field (never a blanket snake→camel pass, which would corrupt genuinely snake_case siblings like
card_id/to_phase_id), and actionmetadatastays a pass-through dict so GET→update round-trips serialize identically.Tested e2e a few times
No new validation rules land here — capability and provider hardening build on this typed base.
Closes #416.