---
title: "Update an formation"
method: PUT
path: "/v1/projects/{project_id}/formations/{formation_id}"
tags: ["Formations"]
---

# Update an formation

`PUT /v1/projects/{project_id}/formations/{formation_id}`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/formations/{formation_id}`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

Applies a new template to the formation. Resources are created, updated, or deleted to reconcile the current state with the desired state.

A **template-shape** error is refused with `400`. A **deploy** failure is not: the operation ran, so the formation is returned with `200` and `status: "failed"`, and `error` explains why. Read `status` — a `2xx` here means the deploy was attempted, not that it worked. The `naturali` CLI exits non-zero on that body so `update-formation && …` does not lie.

A deploy that replaced a resource and could not delete the superseded one answers `status: "active"` with `error.code: "FORMATION_REPLACE_CLEANUP_FAILED"` — the desired state is realised, and `error.meta.failures` names every resource still live. The next deploy retries the disposal.

## Path parameters

- `formation_id` string, required

## Request body

- object
  - `template` union: A formation template supplied as either a JSON object or a YAML/JSON string. When a string is provided the server parses it with a YAML parser (JSON is valid YAML) before processing.
    - FormationTemplate
      - `parameters` object, nullable: Declared parameters for this template. Each parameter may have a default value and an optional description. Parameters without a default must be supplied in the `parameters` field of the deploy request.
      - `resources` object, required: Map of logical resource IDs to resource declarations
      - `outputs` object, nullable: Map of output names to values. Values may use `{ "ref": "logicalId" }` to reference physical IDs of created resources, or `{ "param": "ParamName" }` and `{ "sub": "text ${ParamName}" }` to embed parameter values. `{ "ref_attr": "LogicalId.attribute" }` resolves a named attribute of a created resource, except one carrying credential material — a trigger's or webhook's `secret` is refused with 400 VALIDATION_FAILED, since a formation is readable by anyone holding `formations:GetFormation`. Read those from the resource's own secret route instead.
      - `metadata` NullableMetadataBag, nullable: A `MetadataBag` on a request field where `null` is meaningful: a full-replacement update that clears the bag. A record whose bag is unset leaves the field out.
    - string: YAML or JSON string representation of a FormationTemplate
  - `parameters` object, nullable: Runtime parameter values that override or supply template parameter defaults. Keys must match parameter names declared in `template.parameters`. Required parameters (those without a default) must be provided here, unless the parameter is declared with `use_previous_value: true`, in which case omitting it reuses the previously stored value.
  - `metadata` NullableMetadataBag, nullable: A `MetadataBag` on a request field where `null` is meaningful: a full-replacement update that clears the bag. A record whose bag is unset leaves the field out.

## Response `200`

Updated formation

- Formation
  - `id` string: Public ID of the formation
  - `project_id` string: Project public ID
  - `name` string: Human-readable formation name
  - `template` FormationTemplate
    - `parameters` object, nullable: Declared parameters for this template. Each parameter may have a default value and an optional description. Parameters without a default must be supplied in the `parameters` field of the deploy request.
    - `resources` object, required: Map of logical resource IDs to resource declarations
    - `outputs` object, nullable: Map of output names to values. Values may use `{ "ref": "logicalId" }` to reference physical IDs of created resources, or `{ "param": "ParamName" }` and `{ "sub": "text ${ParamName}" }` to embed parameter values. `{ "ref_attr": "LogicalId.attribute" }` resolves a named attribute of a created resource, except one carrying credential material — a trigger's or webhook's `secret` is refused with 400 VALIDATION_FAILED, since a formation is readable by anyone holding `formations:GetFormation`. Read those from the resource's own secret route instead.
    - `metadata` NullableMetadataBag, nullable: A `MetadataBag` on a request field where `null` is meaningful: a full-replacement update that clears the bag. A record whose bag is unset leaves the field out.
  - `outputs` object, nullable: Resolved output values after stack deployment. An output that resolved a credential attribute on a formation deployed before those were refused is omitted.
  - `status` 'creating' | 'active' | 'updating' | 'failed' | 'deleting' | 'deleted' | 'delete_failed': Formation status
  - `metadata` MetadataBag: Caller-owned annotations on a resource, stored as the object they were written as: the types a value was written with are the types a read returns, so a filter can ask an ordering question about a number. Unlike other body fields, keys are stored and returned verbatim in the casing supplied — they are not converted between snake_case and camelCase. No key is reserved, and that is the point: every piece of state the platform owns lives in its own typed column, so nothing written here reaches platform state. The platform never reads the bag — it is not an IAM context, not a policy input and not part of a prompt — which is what separates it from a tag bag.
  - `resolved_metadata` object, nullable: The template's top-level `metadata` block after parameter (`sub`/`param`) and resource (`ref`) substitution at the last deploy. Null when the template declares no metadata.
  - `resolved_parameters` object, nullable: Parameter values applied at the last deploy, for auditability. `no_echo` parameters are masked (`***`). Null when the template declares no parameters.
  - `error` StoredError: Why a resource's work failed, in the shape of an error response's `error` without `hint` and `docs_url`. Every `error` field a resource records carries it, so `code` can be read without first checking the type of `error`.
    - `code` string, required: A key from the server's ERROR_CODES registry, documented on the Error Codes page. `INTERNAL_ERROR`, with the message `Internal Server Error`, records an unexpected failure whose detail reaches the server log only.
    - `message` string, required
    - `meta` object: The failure's own structured context, when it names any.
  - `resources` FormationResource[]: Resources managed by this formation (present on get/create/update)
    - `id` string: Public ID of the resource record
    - `logical_id` string: Logical identifier from the template
    - `resource_type` string: Resource type (e.g. `agent`, `memory_store`)
    - `physical_resource_id` string, nullable: Public ID of the physical the runtime resource
    - `status` 'pending' | 'created' | 'updated' | 'deleted' | 'failed': Current resource status
  - `created_at` string, date-time
  - `updated_at` string, date-time

## Other responses

- `400`: Bad Request
- `401`: Unauthorized
- `403`: Forbidden — either the caller may not operate on formations in the project, or it lacks an action a resource this template declares requires. `error.meta.denied_actions` names every missing action; nothing is applied.
- `404`: Not Found

---

[API](https://skmtc.dev/naturali/apis/naturali-ai-api.md) · [All operations](https://skmtc.dev/naturali/apis/naturali-ai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/naturali/apis/naturali-ai-api/revisions/e4905b78f422?raw)
