Organize work with workstreams
This guide walks the workstream lifecycle end to end: create a workstream, file a card into it, transfer the card to a different workstream, resolve one of its identifiers (current or aliased), declare a dependency on a card in another board, and link a file to a subtask. Background: Workstreams and identifiers and Card dependencies.
| Resource | Style | Retry safety |
|---|---|---|
| Workstreams | owned resource | POST + Idempotency-Key; PATCH/archive + If-Match |
| Card workstream transfer | versioned mutation | POST + both Idempotency-Key and If-Match |
| Card dependencies | desired-state PUT/DELETE | repeat freely; no headers needed |
| Subtask linked file | desired-state PUT/DELETE | repeat freely; no headers needed |
1. Create a workstream
POST /workstreams creates a
workstream with a workspace-unique, immutable prefix (2 to 10 characters,
must start with a letter, letters and digits only, normalized to uppercase).
It is a normal creating POST, so it requires an Idempotency-Key.
2. Create a card in it
POST /columns/{column_id}/cards accepts an
optional workstream_id. Set it to file the card under the new workstream
instead of the workspace default; the response's identifier reflects the
workstream's prefix (ENG-1, for example).
3. Transfer a card between workstreams
POST /cards/{card_id}/workstream
moves a card to a different active workstream. It requires both
Idempotency-Key and If-Match — it mutates a versioned card and must not
double-transfer on retry. The card gets a new identifier in the target
workstream; the previous identifier becomes a permanent alias, see
Workstreams and identifiers.
4. Resolve an alias
GET /cards/by-identifier/{identifier}
resolves any current or aliased identifier — exact match, case-insensitive —
to the card. Check is_alias and current_identifier in the response to
detect and update stale references.
5. Declare a cross-board dependency
PUT /cards/{card_id}/dependencies/{depends_on_card_id}
declares that a card depends on another card anywhere in the same workspace,
including a different board. It is desired-state — no Idempotency-Key or
If-Match — but rejects a self-dependency (422) and a dependency that
would create a cycle (409).
6. Link a file to a subtask
PUT /subtasks/{subtask_id}/linked-file/{file_id}
links an already-uploaded, clean, visible file to a subtask — at most one
per subtask, replacing any previous link. It is also desired-state;
DELETE /subtasks/{subtask_id}/linked-file
clears it.
cURL
COLUMN_ID="<column-id>"
OTHER_WORKSTREAM_ID="<other-workstream-id>"
OTHER_BOARD_CARD_ID="<card-id-on-another-board>"
FILE_ID="<workspace-file-id>"
# 1. Create a workstream (idempotent POST).
curl -sS -i -X POST "$KANBAN_API_BASE_URL/workstreams" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: guide-create-workstream-001" \
-d '{"name": "Engineering", "prefix": "ENG"}'
WORKSTREAM_ID="<workstream-id-from-the-response>"
# 2. Create a card filed under the new workstream.
curl -sS -i -X POST "$KANBAN_API_BASE_URL/columns/$COLUMN_ID/cards" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: guide-create-card-001" \
-d "{\"title\": \"Ship the workstream guide\", \"workstream_id\": \"$WORKSTREAM_ID\"}"
CARD_ID="<card-id-from-the-response>"
CARD_ETAG='"1"' # from the create response's ETag header
# 3. Transfer the card to another workstream (both headers required).
curl -sS -i -X POST "$KANBAN_API_BASE_URL/cards/$CARD_ID/workstream" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: guide-transfer-card-001" \
-H "If-Match: $CARD_ETAG" \
-d "{\"workstream_id\": \"$OTHER_WORKSTREAM_ID\"}"
# 4. Resolve the card's original identifier — it now reports as an alias.
ORIGINAL_IDENTIFIER="ENG-1"
curl -sS "$KANBAN_API_BASE_URL/cards/by-identifier/$ORIGINAL_IDENTIFIER" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"
# 5. Declare a dependency on a card in another board (desired-state PUT).
curl -sS -X PUT "$KANBAN_API_BASE_URL/cards/$CARD_ID/dependencies/$OTHER_BOARD_CARD_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"
# 6. Add a subtask and link a file to it (desired-state PUT).
curl -sS -i -X POST "$KANBAN_API_BASE_URL/cards/$CARD_ID/subtasks" \
-H "Authorization: Bearer $KANBAN_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: guide-create-subtask-001" \
-d '{"title": "Attach the design doc"}'
SUBTASK_ID="<subtask-id-from-the-response>"
curl -sS -X PUT "$KANBAN_API_BASE_URL/subtasks/$SUBTASK_ID/linked-file/$FILE_ID" \
-H "Authorization: Bearer $KANBAN_API_TOKEN"
JavaScript
const baseUrl =
process.env.KANBAN_API_BASE_URL ?? "https://app.filetag.ai/api/kanban/v1";
const token = process.env.KANBAN_API_TOKEN;
const columnId = process.env.KANBAN_COLUMN_ID;
const otherWorkstreamId = process.env.KANBAN_OTHER_WORKSTREAM_ID;
const otherBoardCardId = process.env.KANBAN_OTHER_BOARD_CARD_ID;
const fileId = process.env.KANBAN_FILE_ID;
async function api(path, options = {}) {
const response = await fetch(`${baseUrl}${path}`, {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...options.headers,
},
});
const payload = response.status === 204 ? null : await response.json();
if (!response.ok) {
throw new Error(
`${payload.error.code}: ${payload.error.message} (request ${payload.error.request_id})`
);
}
return { response, payload };
}
async function main() {
// 1. Create a workstream.
const workstream = await api("/workstreams", {
method: "POST",
headers: { "Idempotency-Key": "guide-create-workstream-001" },
body: JSON.stringify({ name: "Engineering", prefix: "ENG" }),
});
console.log("workstream:", workstream.payload.data.prefix);
// 2. Create a card filed under that workstream.
const created = await api(`/columns/${columnId}/cards`, {
method: "POST",
headers: { "Idempotency-Key": "guide-create-card-001" },
body: JSON.stringify({
title: "Ship the workstream guide",
workstream_id: workstream.payload.data.id,
}),
});
const card = created.payload.data;
const originalIdentifier = card.identifier;
const etag = created.response.headers.get("etag");
console.log("card identifier:", originalIdentifier);
// 3. Transfer the card to another workstream.
const transferred = await api(`/cards/${card.id}/workstream`, {
method: "POST",
headers: {
"Idempotency-Key": "guide-transfer-card-001",
"If-Match": etag,
},
body: JSON.stringify({ workstream_id: otherWorkstreamId }),
});
console.log("new identifier:", transferred.payload.data.identifier);
// 4. Resolve the original identifier — it now reports as an alias.
const resolved = await api(
`/cards/by-identifier/${encodeURIComponent(originalIdentifier)}`
);
console.log(
"is_alias:",
resolved.payload.data.is_alias,
"current:",
resolved.payload.data.current_identifier
);
// 5. Declare a dependency on a card in another board.
await api(`/cards/${card.id}/dependencies/${otherBoardCardId}`, {
method: "PUT",
});
console.log("dependency declared");
// 6. Add a subtask and link a file to it.
const subtask = await api(`/cards/${card.id}/subtasks`, {
method: "POST",
headers: { "Idempotency-Key": "guide-create-subtask-001" },
body: JSON.stringify({ title: "Attach the design doc" }),
});
const linked = await api(
`/subtasks/${subtask.payload.data.id}/linked-file/${fileId}`,
{ method: "PUT" }
);
console.log("linked file:", linked.payload.data.name);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Python
import json
import os
import urllib.parse
import urllib.request
BASE_URL = os.environ.get(
"KANBAN_API_BASE_URL", "https://app.filetag.ai/api/kanban/v1"
)
TOKEN = os.environ["KANBAN_API_TOKEN"]
COLUMN_ID = os.environ["KANBAN_COLUMN_ID"]
OTHER_WORKSTREAM_ID = os.environ["KANBAN_OTHER_WORKSTREAM_ID"]
OTHER_BOARD_CARD_ID = os.environ["KANBAN_OTHER_BOARD_CARD_ID"]
FILE_ID = os.environ["KANBAN_FILE_ID"]
def api(path, method="GET", body=None, headers=None):
data = json.dumps(body).encode("utf-8") if body is not None else None
request = urllib.request.Request(
f"{BASE_URL}{path}",
data=data,
method=method,
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
**(headers or {}),
},
)
with urllib.request.urlopen(request) as response:
payload = json.load(response) if response.status != 204 else None
return response.headers, payload
# 1. Create a workstream.
_, workstream = api(
"/workstreams",
method="POST",
body={"name": "Engineering", "prefix": "ENG"},
headers={"Idempotency-Key": "guide-create-workstream-001"},
)
print("workstream:", workstream["data"]["prefix"])
# 2. Create a card filed under that workstream.
card_headers, created = api(
f"/columns/{COLUMN_ID}/cards",
method="POST",
body={
"title": "Ship the workstream guide",
"workstream_id": workstream["data"]["id"],
},
headers={"Idempotency-Key": "guide-create-card-001"},
)
card = created["data"]
original_identifier = card["identifier"]
etag = card_headers.get("ETag")
print("card identifier:", original_identifier)
# 3. Transfer the card to another workstream.
_, transferred = api(
f"/cards/{card['id']}/workstream",
method="POST",
body={"workstream_id": OTHER_WORKSTREAM_ID},
headers={
"Idempotency-Key": "guide-transfer-card-001",
"If-Match": etag,
},
)
print("new identifier:", transferred["data"]["identifier"])
# 4. Resolve the original identifier — it now reports as an alias.
_, resolved = api(
f"/cards/by-identifier/{urllib.parse.quote(original_identifier)}"
)
print(
"is_alias:",
resolved["data"]["is_alias"],
"current:",
resolved["data"]["current_identifier"],
)
# 5. Declare a dependency on a card in another board.
api(f"/cards/{card['id']}/dependencies/{OTHER_BOARD_CARD_ID}", method="PUT")
print("dependency declared")
# 6. Add a subtask and link a file to it.
_, subtask = api(
f"/cards/{card['id']}/subtasks",
method="POST",
body={"title": "Attach the design doc"},
headers={"Idempotency-Key": "guide-create-subtask-001"},
)
_, linked = api(
f"/subtasks/{subtask['data']['id']}/linked-file/{FILE_ID}", method="PUT"
)
print("linked file:", linked["data"]["name"])
What this does
Creating the workstream and the card are ordinary idempotent POSTs. The
transfer is the interesting step: it allocates a fresh PREFIX-N identifier
in the target workstream and immediately turns the old identifier into a
permanent alias — getCardByIdentifier keeps resolving both, and tells you
which one is current via is_alias/current_identifier. The dependency and
linked-file writes are desired-state, so re-running steps 5 and 6 changes
nothing. See
Workstreams and identifiers and
Card dependencies for the full
semantics.
Failure modes to expect: 404 not_found when the target workstream, card,
or file is not visible to your token; 409 conflict when a dependency would
create a cycle or a new workstream's prefix collides with an existing one;
422 validation_error for a self-dependency or an invalid prefix;
428 precondition_required / 412 precondition_failed on the transfer when
If-Match is missing or stale.