Skip to content
ACS Infra

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

  1. 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
        }
      ]
    }
  2. Put the Loom's id in the shell variable LOOM_ID, and the root's id in NODE_ID.

Branch under a Node

  1. 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 queued and 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
          }
        }
      ]
    }
  2. 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:

FieldDefaultWhat it does
modelnone, requiredThe model that writes the new Nodes.
n1How many child Nodes to make, from 1 to 8.
max_tokens16The most tokens of each new Node, from 1 to 1,000.
temperature1The sampling temperature.
logprobsnullThe number of top tokens whose log probabilities each Node returns for each of its tokens.
seeda new oneNode i of the Branch, from 0, samples with seed + i.
top_p, top_k, min_p, presence_penalty, frequency_penalty, repetition_penalty, stopas in CompletionsHow 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_request for a field that the table does not list.
  • 400 for a value that Completions refuses, such as a top_p of 0 (Strict validation).
  • 400 model_not_found for a model that the API does not serve.
  • 409 when the Node is queued, running, or failed. Branch only under a completed or cancelled Node.
  • 429 budget_exceeded when your tokens cannot hold one Node.
  • 429 spend_cap while 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

  1. Read the Loom:

    curl "$ACS_API_BASE/looms/$LOOM_ID" \
      -H "Authorization: Bearer $ACS_API_KEY"
  2. Read it again until each Node of the Branch is completed, failed, or cancelled.

When both new Nodes are completed, the Loom shows their text:

Loom
{
  "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:

statusMeaning
queuedThe Node waits to run.
runningInference writes the Node. text holds the tokens so far, and grows.
completedThe text is complete. With logprobs in the Branch, logprobs is set.
failedThe Completion failed. text holds the tokens before the failure, and error says why.
cancelledYou 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:

  1. First the API sends each Node of the Loom, in the order of a read.
  2. Then, while a Node is queued or running, the API reads the Loom every 250 ms and sends each Node whose status or text changed. A running Node's text is its whole text so far.
  3. data: [DONE] ends the stream when no Node is queued or running.

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.

ChangeRouteBody
Edit a NodePOST /v1/looms/{loom_id}/nodes/{node_id}{"text": "..."}
Split a NodePOST /v1/looms/{loom_id}/nodes/{node_id}/split{"offset": 5}, or with "text"
Add a siblingPOST /v1/looms/{loom_id}/nodes/{node_id}/sibling{"text": "..."}
Set a new rootPOST /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 request has seed: 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:

  1. Put the id of the Node to split in NODE_ID, such as the first new Node of Follow a Branch.

  2. 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 failed Node, whose text is what came before the failure.
  • A change while a Node that the change touches is queued or running. 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.

Causeerror
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:

Looms
{
  "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: true puts the Loom above the unpinned ones, and pinned: false unpins 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

  1. Put the id of the Node to delete in NODE_ID.

  2. 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"
Deleted
{
  "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 queued or running. Delete it when its Branch ends.

Export and import a Loom

  1. Download a Loom as a file:

    curl "$ACS_API_BASE/looms/$LOOM_ID/export" \
      -H "Authorization: Bearer $ACS_API_KEY" -o loom.json
  2. Make a new Loom from the file. Send it to POST /v1/looms/import with the import command at the end of this section, with -d @loom.json in 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, and status.
  • position, its place among its siblings.
  • logprobs, one entry for each token, with token, logprob, and the likeliest tokens in top.
  • generation_config, the Branch fields it ran with. It also holds the marks text_modified after an edit and context_modified when a Node above it changed. A part of a split sample has slice_offset and slice_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 status is completed.
  • 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.

On this page