---
title: "Set Custom Skill Workspaces"
method: PUT
path: "/skills/{public_id}/datamates"
tags: ["SKILLS"]
---

# Set Custom Skill Workspaces

`PUT /skills/{public_id}/datamates`

Replace the set of workspaces a skill is attached to.

The whole desired set arrives in one request, so associating a skill with
several workspaces is atomic: every id is validated before anything is
written, and a bad id leaves the previous set untouched.

Three locks in a fixed order, and the order is the contract:

1. **The skill row.** ``set_datamates_for_skill`` is read-modify-write — it
   reads the current set and applies a diff — so without this two requests
   naming *disjoint* sets never contend on any workspace row, both read the
   same current set, and both merge their own addition. Starting from ``{}``,
   one asking for ``{1}`` and one for ``{2}`` commit ``{1, 2}``: a set
   neither caller asked for, from an endpoint whose whole contract is
   "replace". The same lock is what stops a concurrent ``DELETE`` leaving
   attachments behind on a tombstoned skill.
2. **The workspaces**, ascending by id, in ``_lock_workspaces``.
3. **The attachment rows**, in ``set_datamates_for_skill``.

Workspaces before attachment rows is what keeps this from closing a cycle
with ``soft_delete_datamate``, which takes ``Datamate`` then
``CustomSkillDatamate`` and never wants ``CustomSkill``.

The response describes the association and nothing else. It used to return
the whole skill, which meant reading ``SKILL.md`` from S3 *after* the commit
— so an S3 blip turned a write that had already landed into a 500, and the
client would retry an operation that had already succeeded. Nothing here
changes the bundle, so there is nothing to read.

## Path parameters

- `public_id` string, required

## Request body

- SetCustomSkillWorkspacesRequest — Replace the whole set of workspaces a skill is attached to. Expressing the desired set in one request keeps the association atomic: attaching three workspaces one call at a time leaves a window where some are applied and some are not if a later call fails.
  - `datamate_ids` integer[]

## Response `200`

Successful Response

- SkillWorkspacesResponse — The result of replacing a skill's workspace attachments. Shaped like what the endpoint changed rather than like the whole skill. The skill's own fields are unaffected by an attachment write, and returning them forced a read of ``SKILL.md`` from S3 after the transaction had committed — which turned a storage blip into a 500 on a write that had already landed.
  - `public_id` string, required
  - `attached_datamate_ids` integer[]

## Other responses

- `403` — Not the skill's creator
- `404` — Skill or workspace not found
- `422` — Validation Error

## Changes

- **2026-09-02** `fcb62c79da95` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/myaltimate/apis/fastapi/changes/skills/:public_id/datamates/put.md)

---

[API](https://skmtc.dev/myaltimate/apis/fastapi.md) · [All operations](https://skmtc.dev/myaltimate/apis/fastapi/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/myaltimate/fastapi/revisions/fcb62c79da95/schema)
