Grow a Loom with the API
A Loom is a tree of Nodes that you grow from one prompt. Use it to compare the texts that a Base model writes, and to continue the ones you choose. This guide grows a Loom from a script. Set ACS_API_BASE and ACS_API_KEY first, as Set up shows. To grow one in the browser, use the App's Loom page.
The root Node holds the prompt. A Branch makes one or more child Nodes under a Node. Each child is one Completion of the text on the path from the root to its parent. The API runs each Branch in the background, so it finishes when no client is connected.
Make a Loom
-
Send the prompt:
curl "$ACS_API_BASE/looms" \ -H "Authorization: Bearer $ACS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "The sky is"}'The API answers the Loom with its root Node, which is
completed:Loom { "id": "01a12e70-9370-7a9b-87d6-4c3d2621969e", "object": "loom", "created_at": 1791780492, "title": "", "pinned": false, "nodes": [ { "id": "01a12e70-9373-7799-8f05-0f6104866ad3", "object": "loom.node", "parent_id": null, "status": "completed", "text": "The sky is", "logprobs": null, "error": null, "request": null } ] } -
Put the Loom's
idin the shell variableLOOM_ID, and the root'sidinNODE_ID.
Branch under a Node
-
Ask for two child Nodes under the root, each with at most 2 tokens and the log probability of each token:
curl "$ACS_API_BASE/looms/$LOOM_ID/nodes/$NODE_ID/branch" \ -H "Authorization: Bearer $ACS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "llama-8b", "max_tokens": 2, "n": 2, "logprobs": 1}'The API answers the new Nodes. They are
queuedand have no text yet:Branch { "object": "list", "data": [ { "id": "01a12e70-97cc-74a1-bb2d-c199187442ef", "object": "loom.node", "parent_id": "01a12e70-9373-7799-8f05-0f6104866ad3", "status": "queued", "text": "", "logprobs": null, "error": null, "request": { "model": "meta-llama/Llama-3.1-8B", "max_tokens": 2, "temperature": 1, "logprobs": 1, "top_p": 1, "top_k": 0, "min_p": 0, "presence_penalty": 0, "frequency_penalty": 0, "repetition_penalty": 1, "seed": 1739262751, "stop": null } }, { "id": "01a12e70-97cc-76d3-a601-15fa41388315", "object": "loom.node", "parent_id": "01a12e70-9373-7799-8f05-0f6104866ad3", "status": "queued", "text": "", "logprobs": null, "error": null, "request": { "model": "meta-llama/Llama-3.1-8B", "max_tokens": 2, "temperature": 1, "logprobs": 1, "top_p": 1, "top_k": 0, "min_p": 0, "presence_penalty": 0, "frequency_penalty": 0, "repetition_penalty": 1, "seed": 1739262752, "stop": null } } ] } -
Follow the new Nodes until they end. Follow a Branch shows the two ways.
A Branch takes these fields. Each one has the type and the default of the same field of POST /v1/completions, except max_tokens and n:
| Field | Default | What it does |
|---|---|---|
model | none, required | The model that writes the new Nodes. |
n | 1 | How many child Nodes to make, from 1 to 8. |
max_tokens | 16 | The most tokens of each new Node, from 1 to 1,000. |
temperature | 1 | The sampling temperature. |
logprobs | null | The number of top tokens whose log probabilities each Node returns for each of its tokens. |
seed | a new one | Node i of the Branch, from 0, samples with seed + i. |
top_p, top_k, min_p, presence_penalty, frequency_penalty, repetition_penalty, stop | as in Completions | How to sample each token, and where the text stops. |
Request fields says what each sampling field does. Without a seed, the API draws one from 0 to 2,147,483,647 for the Branch. With n Nodes, the last Node samples with seed + n - 1. That number must be at most 9,007,199,254,740,991, the largest integer that JSON and JavaScript hold exactly.
The API checks the Branch when you make it, and makes no Node when it refuses one:
- 400
invalid_requestfor a field that the table does not list. - 400 for a value that Completions refuses, such as a
top_pof 0 (Strict validation). - 400
model_not_foundfor a model that the API does not serve. - 409 when the Node is
queued,running, orfailed. Branch only under acompletedorcancelledNode. - 429
budget_exceededwhen your tokens cannot hold one Node. - 429
spend_capwhile new GPU work is paused (Spend limit).
Each Node takes its own Hold when it runs, and its prompt is the whole text of its path. Holds of background work says what that costs.
Sample a Node again
Each child Node keeps its request: the Branch fields that it ran with, its own seed included. To sample a Node again, send its request as a Branch under the same parent. With the same seed, the new Node gets the same tokens, but vLLM can still change a token now and then when it batches requests.
The root's request is null. So is the request of a Node that you wrote as a sibling or a new root. A Node whose fields no longer read as Branch fields has none either. A Node that ran before a range narrowed keeps the value that it ran with, such as a min_p of 2. A Branch with that value gets 400 invalid_request.
Follow a Branch
-
Read the Loom:
curl "$ACS_API_BASE/looms/$LOOM_ID" \ -H "Authorization: Bearer $ACS_API_KEY" -
Read it again until each Node of the Branch is
completed,failed, orcancelled.
When both new Nodes are completed, the Loom shows their text:
{
"id": "01a12e70-9370-7a9b-87d6-4c3d2621969e",
"object": "loom",
"created_at": 1791780492,
"title": "The sky is",
"pinned": false,
"nodes": [
{
"id": "01a12e70-9373-7799-8f05-0f6104866ad3",
"object": "loom.node",
"parent_id": null,
"status": "completed",
"text": "The sky is",
"logprobs": null,
"error": null,
"request": null
},
{
"id": "01a12e70-97cc-74a1-bb2d-c199187442ef",
"object": "loom.node",
"parent_id": "01a12e70-9373-7799-8f05-0f6104866ad3",
"status": "completed",
"text": " crying,",
"logprobs": {
"text_offset": [0, 7],
"token_logprobs": [-4.989488124847412, -1.722730040550232],
"tokens": [" crying", ","],
"top_logprobs": [
{ " the": -1.4269880056381226, " crying": -4.989488124847412 },
{ ",": -1.722730040550232 }
]
},
"error": null,
"request": {
"model": "meta-llama/Llama-3.1-8B",
"max_tokens": 2,
"temperature": 1,
"logprobs": 1,
"top_p": 1,
"top_k": 0,
"min_p": 0,
"presence_penalty": 0,
"frequency_penalty": 0,
"repetition_penalty": 1,
"seed": 1739262751,
"stop": null
}
},
{
"id": "01a12e70-97cc-76d3-a601-15fa41388315",
"object": "loom.node",
"parent_id": "01a12e70-9373-7799-8f05-0f6104866ad3",
"status": "completed",
"text": " the colour",
"logprobs": {
"text_offset": [0, 4],
"token_logprobs": [-1.4269880056381226, -5.764825344085693],
"tokens": [" the", " colour"],
"top_logprobs": [
{ " the": -1.4269880056381226 },
{ " limit": -0.07732537388801575, " colour": -5.764825344085693 }
]
},
"error": null,
"request": {
"model": "meta-llama/Llama-3.1-8B",
"max_tokens": 2,
"temperature": 1,
"logprobs": 1,
"top_p": 1,
"top_k": 0,
"min_p": 0,
"presence_penalty": 0,
"frequency_penalty": 0,
"repetition_penalty": 1,
"seed": 1739262752,
"stop": null
}
}
]
}The root comes first. Each Node comes after its parent and before its next sibling, and siblings come in the order they were made. A Node's text is only its own part. The text of a path is the text of its Nodes from the root down, so a Branch under the second Node continues "The sky is the colour".
The model samples each Node on its own, so with a temperature above 0 the two Nodes have different texts. The first Branch also names the Loom: its title is now the first line of the root.
A Node is in one of these states:
status | Meaning |
|---|---|
queued | The Node waits to run. |
running | Inference writes the Node. text holds the tokens so far, and grows. |
completed | The text is complete. With logprobs in the Branch, logprobs is set. |
failed | The Completion failed. text holds the tokens before the failure, and error says why. |
cancelled | You stopped the Node while it grew. text holds the tokens it grew. |
A Node can wait queued while other Nodes run. Looms in Limits says how many Nodes run at once.
Follow a Loom as a stream
To get each change as it happens instead of reading the Loom again, open the Loom's stream:
curl -N "$ACS_API_BASE/looms/$LOOM_ID/stream" \
-H "Authorization: Bearer $ACS_API_KEY"The stream is Server-Sent Events. Each event is one line that holds data: and a Node object, the same object that a read of the Loom shows:
- First the API sends each Node of the Loom, in the order of a read.
- Then, while a Node is
queuedorrunning, the API reads the Loom every 250 ms and sends each Node whosestatusortextchanged. A running Node'stextis its whole text so far. data: [DONE]ends the stream when no Node isqueuedorrunning.
When the Branch above has ended, the stream sends the three Nodes of Follow a Branch and data: [DONE] at once.
A stream sees each Branch that starts before the stream ends, also a Branch that another client starts. Open the stream again for a Branch that starts after it ended. If a read of the Loom fails after the stream started, the last event before [DONE] is the error object.
Change the tree
You can write a Loom's text yourself. Each change answers the whole Loom.
| Change | Route | Body |
|---|---|---|
| Edit a Node | POST /v1/looms/{loom_id}/nodes/{node_id} | {"text": "..."} |
| Split a Node | POST /v1/looms/{loom_id}/nodes/{node_id}/split | {"offset": 5}, or with "text" |
| Add a sibling | POST /v1/looms/{loom_id}/nodes/{node_id}/sibling | {"text": "..."} |
| Set a new root | POST /v1/looms/{loom_id}/root | {"text": "..."} |
The API stores a CRLF or a lone CR in your text as LF.
An edit rewrites the Node's text and drops its log probabilities. The same text changes nothing. The Nodes under the edited Node keep their own text, so the text of their paths changes with it.
A split cuts the Node at offset characters, counted in Unicode code points. An offset past the end cuts at the end. The Node keeps the text before the cut. A new child gets the text after it, and the Node's children move under that child, so every path keeps its text.
- A split of the Node's own text gives each half the log probabilities of the whole tokens on its side. A token across the cut leaves the new child none.
- With
text, the API cuts that edited text instead, and neither half keeps log probabilities. - The new child's
requesthasseed: null, because part of a sample cannot be sampled again.
A sibling is a Node that you write under the same parent as this Node. A new root replaces the whole tree with one root, your text.
For example, split a Node after its first 7 characters:
-
Put the id of the Node to split in
NODE_ID, such as the first new Node of Follow a Branch. -
Split it:
curl "$ACS_API_BASE/looms/$LOOM_ID/nodes/$NODE_ID/split" \ -H "Authorization: Bearer $ACS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"offset": 7}'
For the Node " crying,", the Node keeps " crying" and the log probability of that token. Its new child holds "," and the log probability of ",".
The API answers 409 and changes nothing in these cases:
- An edit or a split of a
failedNode, whose text is what came before the failure. - A change while a Node that the change touches is
queuedorrunning. For an edit or a split, that is a Node under the changed Node. For a sibling, it is a Node under the parent. For a new root, it is any Node of the Loom. Stop the Branch, or wait until it ends. - A sibling of the root. A root has no sibling.
- A Branch, a split, or a sibling that would give the Loom more Nodes than it holds (Looms in Limits). The message says how many more Nodes fit.
Stop a Branch
Stop every Node of the Loom that is queued or running:
curl -X POST "$ACS_API_BASE/looms/$LOOM_ID/cancel" \
-H "Authorization: Bearer $ACS_API_KEY"The API answers the Loom. Each Node that grew is now cancelled:
- A running Node keeps the text it grew, and you pay for those tokens, as for a stream that you close.
- A queued Node never runs and holds no tokens.
A stop when no Node grows changes nothing. A stopped Node's text is final, so you can grow a Branch under it. A Branch that you grow at once first waits for the API to save the Node's last tokens.
Read a failed Node
A Node fails when its Completion fails after you made the Branch. The Node's status is then failed, its text holds the tokens that came before the failure, and its error is the error object. The error is what POST /v1/completions answers for the same request, without a request_id.
| Cause | error |
|---|---|
| Your tokens cannot hold the Node when it runs. | code budget_exceeded, and the Node does not reach Inference. |
The text of the path and max_tokens are longer than the model's context. | code context_length_exceeded |
| Inference refuses a value, or fails. | message says why. |
| The API key that made the Branch was revoked, or your access is suspended. | code invalid_api_key, and the Node does not reach Inference |
The error's param names a field of the Branch, or is null. A Branch has no prompt field, so a path that is too long has param: null.
To continue after a failure, grow a new Branch under the failed Node's parent. A failed Node takes no Branch, edit, or split, but you can delete it.
List your Looms
curl "$ACS_API_BASE/looms" \
-H "Authorization: Bearer $ACS_API_KEY"The API answers your newest 100 Looms, each with its title and prompt, without its Nodes:
{
"object": "list",
"data": [
{
"id": "01a124ae-b6ad-7b4b-84cf-e62cb48d9f79",
"object": "loom",
"created_at": 1791616792,
"updated_at": 1791616795,
"title": "The sky is",
"pinned": false,
"prompt": "The sky is"
}
]
}Pinned Looms come first, newest pin first. The other Looms follow by their last change, newest first. Each change of a Loom moves it to the top of the unpinned Looms. A Branch, an edit, a split, a sibling, a new root, a deleted Node, and a rename each change it. A pin does not.
A Loom's title is empty until its first Branch names it after the first line of its root, cut to 40 characters.
Rename or pin a Loom
Send a new title, pinned, or both:
curl "$ACS_API_BASE/looms/$LOOM_ID" \
-H "Authorization: Bearer $ACS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Skies", "pinned": true}'The API answers the Loom with its Nodes, with "title": "Skies" and "pinned": true.
- The API trims the title and keeps its first 120 characters. An empty title leaves the Loom untitled, so its next Branch names it again.
pinned: trueputs the Loom above the unpinned ones, andpinned: falseunpins it. A pin does not change the Loom's last change, so an unpinned Loom goes back to its place.
Delete a Node or a Loom
-
Put the id of the Node to delete in
NODE_ID. -
Delete the Node and every Node under it:
curl -X DELETE "$ACS_API_BASE/looms/$LOOM_ID/nodes/$NODE_ID" \ -H "Authorization: Bearer $ACS_API_KEY"The API answers the Loom without them:
Loom { "id": "01a124ae-b6ad-7b4b-84cf-e62cb48d9f79", "object": "loom", "created_at": 1791616792, "title": "The sky is", "pinned": false, "nodes": [ { "id": "01a124ae-b6c2-7415-b99b-cddb466c5a5f", "object": "loom.node", "parent_id": null, "status": "completed", "text": "The sky is", "logprobs": null, "error": null, "request": null } ] }
To delete the whole Loom, send:
curl -X DELETE "$ACS_API_BASE/looms/$LOOM_ID" \
-H "Authorization: Bearer $ACS_API_KEY"{
"id": "01a124ae-b6ad-7b4b-84cf-e62cb48d9f79",
"object": "loom",
"deleted": true
}You cannot undo a delete. A deleted Node or Loom is gone for you at once: no read, list, or Branch finds it.
The API refuses with 409:
- A delete of the root Node. Delete the Loom instead.
- A delete while a Node to delete is
queuedorrunning. Delete it when its Branch ends.
Export and import a Loom
-
Download a Loom as a file:
curl "$ACS_API_BASE/looms/$LOOM_ID/export" \ -H "Authorization: Bearer $ACS_API_KEY" -o loom.json -
Make a new Loom from the file. Send it to
POST /v1/looms/importwith the import command at the end of this section, with-d @loom.jsonin place of its-d '...'body.
The file has the shape of the Loom export of the old platform (version 2). So a file from either one imports into the other. It holds the Loom's title, the model and last_generation_config of its last Branch, and each Node, each parent before its children. A Node in the file has these fields:
id,parent_id,text, andstatus.position, its place among its siblings.logprobs, one entry for each token, withtoken,logprob, and the likeliest tokens intop.generation_config, the Branch fields it ran with. It also holds the markstext_modifiedafter an edit andcontext_modifiedwhen a Node above it changed. A part of a split sample hasslice_offsetandslice_end.error, for a failed Node.
You can also write a file yourself. This one makes a Loom of two Nodes:
curl "$ACS_API_BASE/looms/import" \
-H "Authorization: Bearer $ACS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"loom": {"title": "Skies"}, "nodes": [{"id": "a", "parent_id": null, "text": "The sky is"}, {"id": "b", "parent_id": "a", "text": " blue"}]}'The API answers the new Loom. It reads the file by these rules:
- Each Node gets a new id, and siblings keep the order of their
position. - A Node without a
statusiscompleted. - A Node whose parent is not in the file goes under the file's root when the file has one root. Otherwise it becomes a root, so a Loom can hold several roots.
- A Node whose parents lead back to itself becomes a root.
- The API trims the file's title and cuts it to 120 characters. A file without a title gets
Imported loom. - A file of more Nodes than a Loom holds, or with two Nodes of the same
id, gets 400.
If the API restarts while a Node runs, the Node runs again later. When the API restarts says how.
The API reference lists every field of each Loom route.