Configuration as Code
Use configuration as code when the same agents, models, schemas, and integrations must be reproduced across environments. The supported automation surfaces are the REST API and brewctl; both work with SyntheticBrew Cloud and Enterprise.
Choose an approach
Section titled “Choose an approach”- Use
brewctlfor a directory of declarative, diffable resources. It plans changes and reconciles dependencies for you. - Use REST when an existing deployment pipeline already has its own reconciliation logic or needs to create only a few resources.
- Use Admin for exploratory changes, then export or pull the resulting configuration before treating it as a deployment source of truth.
This guide shows the REST pattern. Knowledge Graph bundles have their own brewctl kg workflow in the Knowledge Graph quick start.
Before you start
Section titled “Before you start”For the complete example, open API Keys and use Connect a coding agent to generate a provision token. That token includes the Models, Agents, and Schemas read/write scopes used below without granting destructive MCP management authority. A narrower custom token must include Models Read/Write, Agents Read/Write, and Schemas Read/Write. Add MCP Read/Write only if you also configure an MCP server. Store the key and provider credentials in your CI secret manager.
export SYNTHETICBREW_URL="https://YOUR_SYNTHETICBREW_ORIGIN"export SYNTHETICBREW_TOKEN="bb_your_scoped_token"export OPENAI_API_KEY="YOUR_PROVIDER_KEY"Do not call /api/v1/auth/local-session from portable automation. That route exists only when an Enterprise operator enables local authentication; Cloud automation uses a scoped key or an accepted signed identity token.
The shell example requires bash, curl, and jq.
Reconcile named resources
Section titled “Reconcile named resources”SyntheticBrew uses stable names in resource URLs. The common idempotency pattern is:
- list the resource;
- find the exact name;
POSTif it is absent;PATCH /{name}if it exists.
Capabilities are different: list an agent’s capabilities, then POST or PUT /{capability-id}. Relations are listed within a schema and created only when the same source-to-target edge is absent.
#!/usr/bin/env bashset -euo pipefail
: "${SYNTHETICBREW_URL:?set the Cloud or Enterprise origin}": "${SYNTHETICBREW_TOKEN:?set a scoped API key}": "${OPENAI_API_KEY:?set the provider key}"
AUTH_HEADER="Authorization: Bearer ${SYNTHETICBREW_TOKEN}"
request_json() { local method="$1" path="$2" body="${3:-}" if [ -n "$body" ]; then curl --fail-with-body --silent --show-error \ -X "$method" "${SYNTHETICBREW_URL}${path}" \ -H "$AUTH_HEADER" \ -H "Content-Type: application/json" \ -d "$body" else curl --fail-with-body --silent --show-error \ -X "$method" "${SYNTHETICBREW_URL}${path}" \ -H "$AUTH_HEADER" fi}
resource_exists() { local list_path="$1" name="$2" request_json GET "$list_path" \ | jq -e --arg name "$name" '.[] | select(.name == $name)' >/dev/null}
upsert_named() { local list_path="$1" name="$2" create_body="$3" patch_body="$4" if resource_exists "$list_path" "$name"; then request_json PATCH "${list_path}/${name}" "$patch_body" >/dev/null printf 'updated %s\n' "$name" else request_json POST "$list_path" "$create_body" >/dev/null printf 'created %s\n' "$name" fi}
# 1. Create a default chat model. The API accepts openai_compatible,# not a separate "openai" provider type.chat_model_create=$(jq -cn --arg key "$OPENAI_API_KEY" '{ name:"primary-model", type:"openai_compatible", kind:"chat", base_url:"https://api.openai.com/v1", model_name:"gpt-5.4-mini", api_key:$key, is_default:true}')chat_model_patch=$(jq -cn --arg key "$OPENAI_API_KEY" '{ base_url:"https://api.openai.com/v1", model_name:"gpt-5.4-mini", api_key:$key, is_default:true}')upsert_named /api/v1/models primary-model "$chat_model_create" "$chat_model_patch"
# 2. Create the embedding model used by Knowledge. embedding_dim is required.embedding_model_create=$(jq -cn --arg key "$OPENAI_API_KEY" '{ name:"docs-embedding", type:"openai_compatible", kind:"embedding", base_url:"https://api.openai.com/v1", model_name:"text-embedding-3-small", embedding_dim:1536, api_key:$key}')embedding_model_patch=$(jq -cn --arg key "$OPENAI_API_KEY" '{ base_url:"https://api.openai.com/v1", model_name:"text-embedding-3-small", embedding_dim:1536, api_key:$key}')upsert_named /api/v1/models docs-embedding \ "$embedding_model_create" "$embedding_model_patch"
# 3. Create a knowledge base. embedding_model_id accepts a tenant-local name# as well as the internal UUID.kb_create='{ "name":"product-docs", "description":"Product documentation", "embedding_model_id":"docs-embedding"}'kb_patch='{ "description":"Product documentation", "embedding_model_id":"docs-embedding"}'upsert_named /api/v1/knowledge-bases product-docs "$kb_create" "$kb_patch"
# 4. Create the entry and specialist agents. PATCH uses model_id; that field# also accepts the model name. A body field named model is a create alias,# not the partial-update field.router_create='{ "name":"router", "model":"primary-model", "lifecycle":"persistent", "system_prompt":"Route detailed research to the researcher and synthesize its result."}'router_patch='{ "model_id":"primary-model", "lifecycle":"persistent", "system_prompt":"Route detailed research to the researcher and synthesize its result."}'upsert_named /api/v1/agents router "$router_create" "$router_patch"
researcher_create='{ "name":"researcher", "model":"primary-model", "lifecycle":"spawn", "system_prompt":"Use linked knowledge and return a concise, sourced result."}'researcher_patch='{ "model_id":"primary-model", "lifecycle":"spawn", "system_prompt":"Use linked knowledge and return a concise, sourced result."}'upsert_named /api/v1/agents researcher \ "$researcher_create" "$researcher_patch"
# Relation responses use internal UUIDs even though create accepts names.agents_json=$(request_json GET /api/v1/agents)router_id=$(jq -er '.[] | select(.name == "router") | .id' <<<"$agents_json")researcher_id=$(jq -er '.[] | select(.name == "researcher") | .id' <<<"$agents_json")
# 5. Link Knowledge to the specialist. The route is idempotent, but the REST# link does not enable the Knowledge capability by itself.request_json POST \ /api/v1/knowledge-bases/product-docs/agents/researcher >/dev/null
# Add or update the Knowledge capability so knowledge_search is available.knowledge_cap_path=/api/v1/agents/researcher/capabilitiesknowledge_cap_id=$(request_json GET "$knowledge_cap_path" \ | jq -r '.[] | select(.type == "knowledge") | .id' | head -n 1)knowledge_cap_body='{"type":"knowledge","enabled":true,"config":{}}'if [ -n "$knowledge_cap_id" ]; then request_json PUT "${knowledge_cap_path}/${knowledge_cap_id}" \ "$knowledge_cap_body" >/dev/nullelse request_json POST "$knowledge_cap_path" "$knowledge_cap_body" >/dev/nullfi
# 6. Add or update the Memory capability on the entry agent.cap_path=/api/v1/agents/router/capabilitiescap_id=$(request_json GET "$cap_path" \ | jq -r '.[] | select(.type == "memory") | .id' | head -n 1)cap_body='{"type":"memory","enabled":true,"config":{}}'if [ -n "$cap_id" ]; then request_json PUT "${cap_path}/${cap_id}" "$cap_body" >/dev/nullelse request_json POST "$cap_path" "$cap_body" >/dev/nullfi
# 7. Create a schema. entry_agent_id accepts the agent name.schema_create='{ "name":"support", "description":"Customer support workflow", "entry_agent_id":"router", "chat_enabled":true}'schema_patch='{ "description":"Customer support workflow", "entry_agent_id":"router", "chat_enabled":true}'upsert_named /api/v1/schemas support "$schema_create" "$schema_patch"
# 8. Create the delegation edge only when it is missing.relations_path=/api/v1/schemas/support/agent-relationsif ! request_json GET "$relations_path" \ | jq -e --arg source "$router_id" --arg target "$researcher_id" \ '.[] | select(.source == $source and .target == $target)' \ >/dev/null; then request_json POST "$relations_path" \ '{"source":"router","target":"researcher","config":{}}' >/dev/nullfi
printf 'ready: POST %s/api/v1/schemas/support/chat\n' "$SYNTHETICBREW_URL"Expected result: repeated runs report updates instead of creating duplicates, and GET /api/v1/schemas/support identifies router as the entry agent with chat enabled.
Upload documents after configuration
Section titled “Upload documents after configuration”The knowledge-base URL also uses its name, not its UUID:
for document in docs/*.pdf docs/*.md; do curl --fail-with-body --silent --show-error \ -X POST "$SYNTHETICBREW_URL/api/v1/knowledge-bases/product-docs/files" \ -H "Authorization: Bearer $SYNTHETICBREW_TOKEN" \ -F "file=@${document}"doneUploads accept TXT, Markdown, CSV, PDF, and DOCX files up to 50 MB. Each response returns a file record with indexing status; wait until it becomes ready or error. There is no separate re-index endpoint. To update a document, delete its existing file entry and then upload the replacement. Uploading the same name again creates a second document.
Add MCP servers deliberately
Section titled “Add MCP servers deliberately”Create an MCP server before referencing it from an agent. The request uses type (http, streamable-http, sse, or stdio), not transport. Remote authentication fields refer to credentials available to the SyntheticBrew deployment; a secret exported only in the CI runner does not automatically exist inside Cloud or Enterprise.
For that reason, configure production MCP credentials through Admin or the operator-approved secret workflow, then add the resulting server name to an agent with PATCH /api/v1/agents/{name} and its mcp_servers field. See MCP Servers for transports and authentication.
Import and export bundles
Section titled “Import and export bundles”For a supported YAML snapshot:
# Secret-free exportcurl --fail-with-body --silent --show-error \ "$SYNTHETICBREW_URL/api/v1/config/export" \ -H "Authorization: Bearer $SYNTHETICBREW_TOKEN" \ -o syntheticbrew-export.yaml
# Reviewed importcurl --fail-with-body --silent --show-error \ -X POST "$SYNTHETICBREW_URL/api/v1/config/import" \ -H "Authorization: Bearer $SYNTHETICBREW_TOKEN" \ -H "Content-Type: application/x-yaml" \ --data-binary @syntheticbrew-export.yamlThe import/export bundle covers agents, models, MCP servers, schemas and relations, and Knowledge Graph bundles. It does not include tenant settings, knowledge-base documents, capability bindings, API tokens, runtime sessions, or secrets. Import merges supported resources; it is not a universal prune operation.
Configuration export and brewctl kg pull each include at most 500 entities per Knowledge Graph entity type. For larger graphs, back up the maintained bundle or export every page through the REST entity-list endpoint. Knowledge Graphs are applied after the other imported resources; a graph failure does not undo model, MCP-server, agent, or schema changes that succeeded earlier.
Operational cautions
Section titled “Operational cautions”- Review
PATCHbodies carefully. Arrays such asmcp_servers, tools, and knowledge-base memberships replace the current value when supplied. PUTis a full replacement for most named resources. PreferPATCHin reconciliation scripts.- Resource names are immutable URL handles. A rename is create, migrate references, then delete.
- Configuration automation does not migrate PostgreSQL. Enterprise upgrades must run release-matched migrations first.
- Treat the checked-in configuration as sensitive even without secrets: prompts, URLs, graph schemas, and integration names can reveal internal design.