Skip to content

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.

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 --> CHAT

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.

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.

| Field | Type | Notes | | ----------------- | -------------- | --------------------------------------------------------------------------------------- | | instance_id | UUID7 | Primary key | | org_id | UUID7 | Owning org | | name | string | 5–200 characters | | framework_type | string | langgraph | openai_agentsimmutable after creation | | mode | string | supervisor | coordinator | handoff | groundedimmutable 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 |

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.

| 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).

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.

validate_create_request runs before the instance row is created:

  1. Quota check — Counts active orchestrators for the org and enforces the max_orchestrators quota from the subscription tier definition. Raises QuotaExceededError if exceeded.
  2. Plugin uniqueness — Each plugin may only appear once (per source + pid). Duplicate entries raise ValidationError.
  3. Grounded mode constraint — If mode = "grounded", exactly one plugin must be active, and that plugin must declare is_scoped = true (its agent extends BaseScopedAgent). Otherwise raises ValidationError.
cadence/domain/orchestrator/validator.py
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 mode

validate_orchestrator_config runs on create and on config updates:

  1. Framework × mode compatibility — Checks FRAMEWORK_SUPPORTED_MODES for the framework. Raises ValidationError if the mode is not supported (e.g., grounded with openai_agents).
  2. LLM config existence — Extracts all llm_config_id references from the config (top-level default_llm_config_id plus per-node overrides in mode_config). For each, verifies the LLM config exists and is not soft-deleted.
  3. LLM config enabled — Raises ValidationError if a referenced LLM config is disabled (is_enabled = false).
  4. Provider × framework compatibility — Checks FRAMEWORK_SUPPORTED_PROVIDERS for the framework. Raises ValidationError if the LLM config’s provider is not supported (e.g., anthropic with openai_agents).
  5. Prompt placeholder validation — If mode_config contains 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

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.

  1. Create an LLM configuration for the org with your provider credentials and note its id. See LLM configuration.

  2. Confirm that your chosen provider is compatible with the framework. Use GET /api/frameworks/{framework_type}/supported-providers to check at runtime, or consult the compatibility table above.

  3. Post to POST /api/orgs/{org_id}/orchestrators with all required fields. Both framework_type and mode are immutable — choose them carefully.

  4. Load the instance into the pool when ready to serve traffic. See Hot-reload and AI App pool.

cadence/api/orchestrator/schemas.py
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)
POST /api/orgs/{org_id}/orchestrators
{
"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
}
}
}

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):

cadence/api/orchestrator/crud.py
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_ready

pool.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.

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.

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.

cadence/api/orchestrator/schemas.py
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] = None

| 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"} |