Orchestration backends
LangGraph and OpenAI Agents SDK — framework capabilities, provider compatibility, orchestrator CRUD, and config reference.
Intended audience: Stakeholders, Business analysts, Solution architects, Developers, Testers
Learning outcomes by role
Stakeholders
- Compare LangGraph and OpenAI Agents SDK to choose the right engine for your AI product.
Business analysts
- Document the framework × mode matrix and provider constraints as orchestrator configuration criteria.
Solution architects
- Map provider credentials and network egress paths for each active backend.
Developers
- Configure and create orchestrator instances using the correct framework, mode, and LLM config.
Testers
- Build a compatibility matrix of framework × mode × provider for integration test coverage.
Choosing a backend means choosing the engine that will execute your agent graph — how tool calls get routed, how agents hand off to each other, and which model providers the framework can talk to. Production orchestrators use langgraph or openai_agents. The google_adk value appears in create-request validation patterns for forward compatibility, but OrchestratorFactory does not register google_adk backends — do not use it for new instances until support ships.
The framework_type and mode fields are frozen at orchestrator creation. Once an instance exists, switching engines means creating a new orchestrator and redirecting any central points that reference the old one.
How it works
Section titled “How it works”When a chat request arrives, the platform resolves the orchestrator instance from the pool and calls its framework-specific astream method. That method uses the resolved LLM config (decrypted BYOK key, provider, base URL) to make model calls through the framework’s native SDK — LangChain for LangGraph, the OpenAI Agents SDK for openai_agents. Plugins inject tools into the graph at load time, before the first message arrives.
flowchart TD CREATE["POST /api/orgs/org_id/orchestrators"] --> VALIDATE["Schema validates framework_type and mode"] VALIDATE --> DB[(OrchestratorInstance row in PostgreSQL)] DB --> POOL["Pool loads instance into memory"] POOL --> ENGINE{framework_type?} ENGINE -->|langgraph| LG["LangGraph engine — LangChain BaseChatModel"] ENGINE -->|openai_agents| OA["OpenAI Agents SDK — OpenAIChatCompletionsModel or LitellmModel"] LG --> CHAT["Chat request resolved"] OA --> CHATFramework capabilities
Section titled “Framework capabilities”Two backends have real orchestrator implementations today: langgraph (with a full LangGraph pipeline for
supervisor and grounded) and google_adk (with a real adapter and streaming wrapper but no registered
modes). openai_agents has 7 modes registered in _BACKEND_CONFIGS but every one of them is a
create_not_implemented_orchestrator(...) placeholder — calling them raises UnsupportedOperationError.
| Framework | Modes registered in factory (_BACKEND_CONFIGS) | Real implementation? | Provider support (FRAMEWORK_SUPPORTED_PROVIDERS) |
| --------------- | ------------------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------- |
| langgraph | supervisor, coordinator, handoff, grounded, pipeline, reflection | supervisor and grounded; others are placeholders | All providers listed in cadence/core/constants/framework.py |
| openai_agents | supervisor, coordinator, handoff, pipeline, reflection, ensemble, hierarchical | All placeholders | openai, litellm, bifrost |
| google_adk | none (real adapter + streaming wrapper exist, no orchestrators wired) | n/a (adapters only) | litellm, bifrost, claude, anthropic |
langgraph uses the LangChain ecosystem and supports stateful graphs with all registered providers. openai_agents
uses the OpenAI Agents SDK and is limited to providers that implement the OpenAI chat completions interface.
The API validation constant FRAMEWORK_SUPPORTED_MODES (core/constants/framework.py:31-35) is currently
incomplete — it lists {supervisor, grounded} for langgraph and empty sets for the other two frameworks,
so the validator at domain/orchestrator/validator.py:145-147 would reject openai_agents/supervisor even
though the factory has a placeholder for it. Treat the factory registry as the source of truth until that
constant is refreshed.
Provider × framework compatibility
Section titled “Provider × framework compatibility”The FRAMEWORK_SUPPORTED_PROVIDERS constant in cadence/core/constants/framework.py lists these provider sets
(only the names registered in PROVIDER_REGISTRY are usable at runtime; "claude" and "gemini" are listed for
langgraph but are aliases, not registered classes):
| Provider | langgraph | openai_agents | google_adk |
| ------------ | ----------- | --------------- | ------------ |
| openai | ✓ | ✓ | ✗ |
| anthropic | ✓ | ✗ | ✗ |
| claude* | alias | ✗ | ✗ |
| google | ✓ | ✗ | ✗ |
| gemini* | alias | ✗ | ✗ |
| azure | ✓ | ✗ | ✗ |
| groq | ✓ | ✗ | ✗ |
| litellm | ✓ | ✓ | ✓ |
| tensorzero | ✓ | ✗ | ✗ |
| bifrost | ✓ | ✓ | ✓ |
*claude and gemini are listed in FRAMEWORK_SUPPORTED_PROVIDERS[langgraph] but are not in PROVIDER_REGISTRY;
using them as a provider raises ConfigurationError("Unknown provider: claude"). Treat anthropic and google
as the canonical names.
You can query the supported providers for any framework at runtime via
GET /api/frameworks/{framework_type}/supported-providers. Creating an orchestrator with a mismatched provider
and framework will fail at LLM config validation.
Data model
Section titled “Data model”OrchestratorInstance
Section titled “OrchestratorInstance”| Field | Type | Notes |
| ----------------- | -------------- | --------------------------------------------------------------------------------------- |
| instance_id | UUID7 | Primary key |
| org_id | UUID7 | Owning org |
| name | string | 5–200 characters |
| framework_type | string | langgraph | openai_agents — immutable after creation |
| mode | string | supervisor | coordinator | handoff | grounded — immutable after creation |
| status | string | active | suspended | inactive |
| tier | string | hot | demand — target pool tier |
| whoami | string | null | Identity context injected into answer nodes as a system prompt |
| config | JSON | Mutable Tier 4 settings (see below) |
| plugin_settings | JSON | Active plugin settings map (pid → {id, name, settings}) |
| config_hash | string | null | SHA-256 of the effective config — used as a pool cache invalidation key |
| is_ready | boolean | true if the instance is currently loaded in the pool (computed at query time) |
| is_deleted | boolean | Soft delete |
| created_at | ISO-8601 | UTC |
| updated_at | ISO-8601 | UTC |
config object
Section titled “config object”The config field is a JSON object with these common keys:
| Key | Type | Purpose |
| ----------------------------------------- | ----------- | ---------------------------------------------------------------------------------- |
| default_llm_config_id | UUID string | LLM configuration to use for model calls |
| mode_config | object | Mode-specific settings (see Orchestration modes) |
| mode_config.max_agent_hops | int | Maximum recursive agent invocations before the graph terminates |
| mode_config.max_tool_rounds | int | Maximum rounds of tool calls per agent turn |
| mode_config.enabled_parallel_tool_calls | boolean | Allow concurrent tool invocations within a single turn |
| mode_config.node_execution_timeout | int | Per-node timeout in seconds |
| mode_config.enabled_llm_validation | boolean | Run a secondary LLM pass to validate agent outputs |
For grounded mode, additional keys apply: scope_rules, enabled_validator, message_context_window, max_context_window, and per-node overrides (router_node, planner_node, synthesizer_node, error_handler_node). See Orchestration modes for the full grounded config reference.
API reference
Section titled “API reference”| Method | Path | Permission | Description |
| -------- | ------------------------------------------------------- | --------------------------------- | ------------------------------------------------------- |
| POST | /api/orgs/{org_id}/orchestrators | cadence:org:orchestrators:write | Create a new orchestrator instance |
| GET | /api/orgs/{org_id}/orchestrators | cadence:org:orchestrators:read | List org orchestrators |
| GET | /api/orgs/{org_id}/orchestrators/{instance_id} | cadence:org:orchestrators:write | Get full orchestrator details (note: gated by _WRITE, not _READ) |
| PATCH | /api/orgs/{org_id}/orchestrators/{instance_id} | cadence:org:orchestrators:write | Update name, tier, whoami, default_llm_config_id |
| PATCH | /api/orgs/{org_id}/orchestrators/{instance_id}/config | cadence:org:orchestrators:write | Replace the mutable config object |
| PATCH | /api/orgs/{org_id}/orchestrators/{instance_id}/status | cadence:org:orchestrators:write | Set status to active or suspended |
| GET | /api/orgs/{org_id}/orchestrators/{instance_id}/graph | cadence:org:orchestrators:write | Return Mermaid graph definition (loaded instances only) |
| DELETE | /api/orgs/{org_id}/orchestrators/{instance_id} | cadence:org:orchestrators:write | Deactivate (org_admin) or soft-delete (sys_admin) |
| DELETE | /api/orgs/{org_id}/orchestrators/{instance_id}/purge | cadence:system:admin | Permanently delete a soft-deleted orchestrator |
| POST | /api/orgs/{org_id}/orchestrators/{instance_id}/load | cadence:org:orchestrators:lifecycle | Load the instance into the pool (202) |
| POST | /api/orgs/{org_id}/orchestrators/{instance_id}/unload | cadence:org:orchestrators:lifecycle | Unload the instance from the pool (202) |
| GET | /api/frameworks | Authenticated | List registered framework types |
| GET | /api/frameworks/{framework_type}/supported-providers | Authenticated | Query provider and mode support for a framework |
There is no POST .../reload route — to reload a running instance, the caller must issue unload then load
(or trigger a config_hash-based reload via the orchestrator.reload event in the message bus).
Config validation pipeline
Section titled “Config validation pipeline”When you create or update an orchestrator, the platform runs a multi-stage validation pipeline before persisting the instance. All validation logic lives in cadence/domain/orchestrator/validator.py.
Create request validation
Section titled “Create request validation”validate_create_request runs before the instance row is created:
- Quota check — Counts active orchestrators for the org and enforces the
max_orchestratorsquota from the subscription tier definition. RaisesQuotaExceededErrorif exceeded. - Plugin uniqueness — Each plugin may only appear once (per
source+pid). Duplicate entries raiseValidationError. - Grounded mode constraint — If
mode = "grounded", exactly one plugin must be active, and that plugin must declareis_scoped = true(its agent extendsBaseScopedAgent). Otherwise raisesValidationError.
if params.mode == "grounded": if len(params.active_plugin_ids) != 1: raise ValidationError("Grounded mode requires exactly one plugin", field="active_plugin_ids") # ... plugin must support grounded modeConfig validation
Section titled “Config validation”validate_orchestrator_config runs on create and on config updates:
- Framework × mode compatibility — Checks
FRAMEWORK_SUPPORTED_MODESfor the framework. RaisesValidationErrorif the mode is not supported (e.g.,groundedwithopenai_agents). - LLM config existence — Extracts all
llm_config_idreferences from the config (top-leveldefault_llm_config_idplus per-node overrides inmode_config). For each, verifies the LLM config exists and is not soft-deleted. - LLM config enabled — Raises
ValidationErrorif a referenced LLM config is disabled (is_enabled = false). - Provider × framework compatibility — Checks
FRAMEWORK_SUPPORTED_PROVIDERSfor the framework. RaisesValidationErrorif the LLM config’s provider is not supported (e.g.,anthropicwithopenai_agents). - Prompt placeholder validation — If
mode_configcontains custom prompts for any node, validates that:- No bare positional
{}placeholders are used - All required placeholders from the default template are present
- No undefined placeholders are referenced
- No bare positional
LLM config ID extraction
Section titled “LLM config ID extraction”extract_llm_config_ids scans these config locations for llm_config_id references:
- Top-level:
config.default_llm_config_id - Per-node in
mode_config:router_node,planner_node,synthesizer_node,validation_node,clarifier_node,responder_node,error_handler_node,agent_node,autocompact,suggestion_node
This allows each graph node to use a different LLM model while ensuring all referenced configs are valid and compatible with the framework.
Creating an orchestrator
Section titled “Creating an orchestrator”-
Create an LLM configuration for the org with your provider credentials and note its
id. See LLM configuration. -
Confirm that your chosen provider is compatible with the framework. Use
GET /api/frameworks/{framework_type}/supported-providersto check at runtime, or consult the compatibility table above. -
Post to
POST /api/orgs/{org_id}/orchestratorswith all required fields. Bothframework_typeandmodeare immutable — choose them carefully. -
Load the instance into the pool when ready to serve traffic. See Hot-reload and AI App pool.
class CreateOrchestratorRequest(BaseModel): name: str = Field(..., min_length=5, max_length=200) framework_type: str = Field( ..., pattern="^(langgraph|openai_agents|google_adk)$" ) mode: str = Field( ..., pattern="^(supervisor|coordinator|handoff|grounded)$" ) active_plugin_ids: List[str] = Field(..., min_length=1) tier: str = Field(default="demand", pattern="^(hot|demand)$") whoami: Optional[str] = None config: Optional[Dict[str, Any]] = Field(default_factory=dict)Example create body
Section titled “Example create body”{ "name": "Customer Support Agent", "framework_type": "langgraph", "mode": "supervisor", "active_plugin_ids": ["com.example.support-tools"], "tier": "hot", "config": { "default_llm_config_id": "01936a8f-...", "mode_config": { "max_agent_hops": 25, "max_tool_rounds": 3, "enabled_parallel_tool_calls": true, "node_execution_timeout": 60, "enabled_llm_validation": false } }}How it works — is_ready flag
Section titled “How it works — is_ready flag”is_ready is not stored in the database. It is computed at query time by asking the pool whether the
instance is currently loaded (in src/cadence/api/orchestrator/crud.py:37-42):
def _check_is_ready(pool: OrchestratorPool, instance_id: str, status: str | None = None) -> bool: if status == "inactive": return False orchestrator = pool.get_loaded(instance_id) return orchestrator is not None and orchestrator.is_readypool.get_loaded peeks the demand pool without extending the TTL. orchestrator.is_ready is the
BaseOrchestrator property set to True only after _build_resources() succeeds inside initialize().
An orchestrator with is_ready = false will reject chat requests with a 503. Load it first via
POST /api/orgs/{org_id}/orchestrators/{instance_id}/load.
How it works — config_hash
Section titled “How it works — config_hash”Every time the orchestrator config is updated, the platform recomputes a config_hash — the first 16 hex
chars of SHA-256 over {"config": config, "plugin_settings": plugin_settings, "whoami": whoami} (computed in
OrchestratorConfigMixin.compute_config_hash, src/cadence/domain/orchestrator/config.py:136-148). The full
digest is stored in the DB column orchestrator_instances.config_hash (nullable, 64 chars). The pool keeps
its own copy in pool._hashes[instance_id]; the event consumer compares the message config_hash against
pool.get_hash(instance_id) and skips the reload if the hash is unchanged. The DB and pool hashes are also
checked when _load_from_db runs: a mismatch on a currently-loaded instance triggers an automatic reload
on the next request.
Updating an orchestrator
Section titled “Updating an orchestrator”name, tier, whoami, and default_llm_config_id can be patched individually without reloading the instance. The full config object can be replaced atomically via PATCH .../config. Neither operation interrupts currently-running conversations — the new config takes effect on the next pool load.
class UpdateOrchestratorMetadataRequest(BaseModel): name: Optional[str] = Field(None, min_length=10, max_length=200) tier: Optional[str] = Field(None, pattern="^(hot|demand)$") whoami: Optional[str] = None default_llm_config_id: Optional[str] = NoneTroubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
| ---------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------- |
| 422 on create — framework_type | Typo or unsupported value | Use langgraph or openai_agents |
| 422 on create — mode | Mode value not in allowed set | Use supervisor, coordinator, handoff, or grounded |
| 422 active_plugin_ids | Empty list | Provide at least one plugin ID |
| ConfigurationError at pool load | framework_type + mode combination not in factory registry | grounded requires langgraph; verify the combination |
| is_ready = false after create | Instance not yet loaded into pool | Call POST .../load; see Hot-reload |
| 503 on chat | Instance not loaded | Load the instance first |
| Need to switch framework or mode | Both fields are immutable | Create a new orchestrator; update central points to reference it |
| 409 on delete | Orchestrator is referenced by a central point | Remove central point references before deleting |
| 410 Gone on get | Orchestrator has been deactivated | Reactivate via PATCH .../status with {"status": "active"} |