Skip to main content

Deploy a system from a template

By the end of this tutorial you will have a small agentic system — a managed model provider, an order-lookup tool and a support agent that calls it — declared in one formation template, deployed with one call, and changed in place by editing the template.

Six steps:

  1. Write and validate the template.
  2. Deploy it.
  3. Run the deployed agent.
  4. Plan a change.
  5. Apply it.
  6. Run the agent again.

Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.

Prerequisites​

  1. A credential. A nat_sk_… API key (or a session JWT from Auth) exported as NATURALI_TOKEN, and your client set up — the CLI, the SDK or plain curl against https://api.naturali.ai/v1:

    export NATURALI_TOKEN=nat_sk_...
    export NATURALI_API=https://api.naturali.ai/v1 # curl examples only
  2. A project. The one you used in Your first agent generation works; the template brings its own provider, so no PROVIDER is needed:

    export PROJECT=proj_cT9LACJi0WypPf5U
  3. jq, for the curl examples: it puts the YAML file into the JSON body.

Let your assistant write the template

A template is plain YAML, which is exactly what an assistant connected over MCP is good at. Describe the system, and it can write the template, validate it and deploy it with the same calls as below.

1. Write and validate the template​

Save this as stack.yaml. Three resources: Models is the managed offering, OrderLookup an http tool, and Support the agent. ref stands in for an id that does not exist yet, so the agent names its provider and its tool before either is created.

resources:
Models:
type: naturali_ai_provider
properties:
name: stack-models
default_model: glm-4.7-flash
OrderLookup:
type: tool
properties:
name: lookup-order
type: http
description: Looks up an order by its id.
parameters:
type: object
properties:
order_id:
type: string
required:
- order_id
execute:
url: https://httpbin.org/anything/orders/{order_id}
method: GET
Support:
type: agent
properties:
name: stack-support
ai_provider_id:
ref: Models
instructions: >-
You answer questions about orders. Look the order up with
lookup-order before answering, and say which URL you checked.
tool_bindings:
- tool_id:
ref: OrderLookup
outputs:
agent_id:
ref: Support

The tool calls httpbin.org, which echoes the request back — in your system it is your order API. Validate before deploying: it parses and type-checks the template and creates nothing.

naturali validate-formation \
--project-id "$PROJECT" \
--template "$(cat stack.yaml)"
{ "valid": true, "errors": [], "warnings": [] }

A template may only declare the resource types this API serves; api_key, policy or webhook answer 400 unsupported_resource_type — see which resource types this API accepts.

2. Deploy it​

POST /v1/projects/{project_id}/formations creates the three resources in dependency order and answers once they exist.

naturali create-formation \
--project-id "$PROJECT" \
--name support-stack \
--template "$(cat stack.yaml)"
{
"id": "form_EPis15Nfukary167",
"name": "support-stack",
"status": "active",
"outputs": { "agent_id": "agent_EmeXFnXV6RRtFHvp" },
"error": null,
"resources": [
{
"logical_id": "Models",
"resource_type": "naturali_ai_provider",
"physical_resource_id": "aip_eHPAQ6qpUkB70D3N",
"status": "created"
},
{
"logical_id": "OrderLookup",
"resource_type": "tool",
"physical_resource_id": "tool_8Vfntfb4p4z2goON",
"status": "created"
},
{
"logical_id": "Support",
"resource_type": "agent",
"physical_resource_id": "agent_EmeXFnXV6RRtFHvp",
"status": "created"
}
]
}

Each physical_resource_id is an ordinary resource, readable through its own module. The outputs block hands you the agent's id:

export FORMATION=form_EPis15Nfukary167
export AGENT=agent_EmeXFnXV6RRtFHvp

3. Run the deployed agent​

naturali create-agent-generation \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--wait true \
--messages '[{"role":"user","content":"Where is order 1001?"}]'
{
"id": "gen_xNbNEQalLzlMvTkm",
"trace_id": "trace_GjICOF5cL7NJ7Kjj",
"status": "completed",
"ai_provider_id": "aip_eHPAQ6qpUkB70D3N",
"output": {
"model": "glm-4.7-flash",
"content": "I checked the order at URL: https://httpbin.org/anything/orders/1001\n\nUnfortunately, the lookup returned an empty data response, which means order 1001 either:\n- Doesn't exist in the system\n- Has no tracking information available\n- Failed to be found\n\nCould you double-check the order number…",
"finish_reason": "stop"
}
}

ai_provider_id is the Models resource, and the reply names the URL the tool called: every piece the template declared took part. The echo carries no order data, so the agent guesses at why — the next step fixes that.

4. Plan a change​

Edit stack.yaml: add two sentences to the agent's instructions, so Support reads:

Support:
type: agent
properties:
name: stack-support
ai_provider_id:
ref: Models
instructions: >-
You answer questions about orders. Look the order up with
lookup-order before answering, and say which URL you checked.
If the lookup returns no order data, say the order is still
being processed and will update within a day.
tool_bindings:
- tool_id:
ref: OrderLookup

POST /v1/projects/{project_id}/formations/plan with the formation's id compares the edited template against what is deployed, and creates nothing.

naturali plan-formation \
--project-id "$PROJECT" \
--formation-id "$FORMATION" \
--template "$(cat stack.yaml)"
{
"changes": [
{
"logical_id": "Models",
"resource_type": "naturali_ai_provider",
"action": "no-op",
"physical_resource_id": "aip_eHPAQ6qpUkB70D3N"
},
{
"logical_id": "OrderLookup",
"resource_type": "tool",
"action": "no-op",
"physical_resource_id": "tool_8Vfntfb4p4z2goON"
},
{
"logical_id": "Support",
"resource_type": "agent",
"action": "update",
"physical_resource_id": "agent_EmeXFnXV6RRtFHvp",
"diff": {
"desired": {
"instructions": "You answer questions about orders. Look the order up with lookup-order before answering, and say which URL you checked. If the lookup returns no order data, say the order is still being processed and will update within a day."
},
"current": {
"instructions": "You answer questions about orders. Look the order up with lookup-order before answering, and say which URL you checked."
}
}
}
]
}

One update, two no-ops: the agent changes in place and keeps its id. Each diff also carries the unchanged properties, trimmed here.

5. Apply it​

PUT /v1/projects/{project_id}/formations/{formation_id} converges the stack on the edited template.

naturali update-formation \
--project-id "$PROJECT" \
--formation-id "$FORMATION" \
--template "$(cat stack.yaml)"
{
"id": "form_EPis15Nfukary167",
"status": "active",
"outputs": { "agent_id": "agent_EmeXFnXV6RRtFHvp" },
"resources": [
{
"logical_id": "Models",
"physical_resource_id": "aip_eHPAQ6qpUkB70D3N",
"status": "created"
},
{
"logical_id": "OrderLookup",
"physical_resource_id": "tool_8Vfntfb4p4z2goON",
"status": "created"
},
{
"logical_id": "Support",
"physical_resource_id": "agent_EmeXFnXV6RRtFHvp",
"status": "updated"
}
]
}

The same agent id, now updated. Like any agent write, the change archived a version: the agent is at version 2.

6. Run the agent again​

Ask the same question as in step 3.

naturali create-agent-generation \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--wait true \
--messages '[{"role":"user","content":"Where is order 1001?"}]'
{
"id": "gen_W1OWLuDVtAd66lFN",
"status": "completed",
"output": {
"content": "I checked https://httpbin.org/anything/orders/1001, but that URL doesn't return any order data. Based on the response, it appears that order 1001 is still being processed. The order should update within a day. Would you like me to check again later, or is there anything else I can help you with?"
}
}

The answer follows the new instructions. The system changed by editing one file, and the agent, its id and its history stayed where they were.

Clean up​

Deleting the formation deletes everything it created. An agent that has run is the exception: the teardown answers 409 FORMATION_DELETE_FAILED and deletes nothing, because the agent has generations and traces. Force-delete the agent first — that removes its history too — then the formation.

naturali delete-agent \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--force true
naturali delete-formation \
--project-id "$PROJECT" \
--formation-id "$FORMATION"
{ "success": true }

To keep a resource when its formation goes, set deletion_policy: retain on it in the template.

What's next​