Skip to content

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.

  • Use brewctl for 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.

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.

Terminal window
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.

SyntheticBrew uses stable names in resource URLs. The common idempotency pattern is:

  1. list the resource;
  2. find the exact name;
  3. POST if it is absent;
  4. 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 bash
set -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/capabilities
knowledge_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/null
else
request_json POST "$knowledge_cap_path" "$knowledge_cap_body" >/dev/null
fi
# 6. Add or update the Memory capability on the entry agent.
cap_path=/api/v1/agents/router/capabilities
cap_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/null
else
request_json POST "$cap_path" "$cap_body" >/dev/null
fi
# 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-relations
if ! 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/null
fi
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.

The knowledge-base URL also uses its name, not its UUID:

Terminal window
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}"
done

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

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.

For a supported YAML snapshot:

Terminal window
# Secret-free export
curl --fail-with-body --silent --show-error \
"$SYNTHETICBREW_URL/api/v1/config/export" \
-H "Authorization: Bearer $SYNTHETICBREW_TOKEN" \
-o syntheticbrew-export.yaml
# Reviewed import
curl --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.yaml

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

  • Review PATCH bodies carefully. Arrays such as mcp_servers, tools, and knowledge-base memberships replace the current value when supplied.
  • PUT is a full replacement for most named resources. Prefer PATCH in 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.