Guides
Practical, step-by-step walkthroughs for the most common Tack integration patterns. Each guide includes working code you can adapt to your stack.
AI-managed project board
Let an LLM agent read your backlog, triage incoming cards, set priorities, and move work through your board. Tack's flat JSON responses and batch operations make it straightforward for agents to operate boards without specialized tooling.
Steps
- 1Give the agent a Tack API key with write access to the target workspace
- 2The agent fetches all cards in the project, filtering by list and priority
- 3It reads card titles, bodies, and labels to understand the current state
- 4Based on its instructions, it triages new cards: sets priority, assigns labels, moves to the correct list
- 5It uses batch-move to reorganize the backlog in a single API call
- 6It posts a summary comment on each card it touched, explaining the reasoning
Code example
// Agent: triage the backlog
const cards = await fetch(
'/tack/v1/workspaces/acme-eng/projects/q1-roadmap/cards?list=backlog&completed=false',
{ headers: { 'Authorization': 'Bearer ' + TACK_API_KEY } }
).then(r => r.json());
// Agent decides priorities based on card content
const triaged = await llm.triage(cards.data);
// Batch-move cards that need immediate attention
await fetch(
'/tack/v1/workspaces/acme-eng/projects/q1-roadmap/cards/batch-move',
{
method: 'POST',
headers: {
'Authorization': 'Bearer ' + TACK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
moves: triaged
.filter(c => c.priority === 'critical')
.map(c => ({
card_id: c.id,
list: 'in-progress',
position: 0,
})),
}),
}
);
// Post triage summary on each card
for (const card of triaged) {
await fetch(
`/tack/v1/workspaces/acme-eng/projects/q1-roadmap/cards/${card.id}/comments`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer ' + TACK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
body: `**Auto-triage**: Set priority to ${card.priority}. ${card.reasoning}`,
}),
}
);
}CI pipeline integration
Connect Tack to your CI/CD workflow. Create cards from failed tests, move cards to "Done" when a fix ships, and link activity back to commits and pull requests.
Steps
- 1In your CI pipeline, detect test failures or build errors
- 2Create a Tack card with the failure details in the body
- 3Assign the card to the on-call engineer or a triage label
- 4When the fix merges, move the card to "Done" and post a comment with the commit SHA
- 5Use the activity feed to correlate deployment events with card movements
Code example
// CI script: create a card for a failed test
async function reportFailure(testName, errorLog, commitSha) {
const card = await fetch(
'/tack/v1/workspaces/acme-eng/projects/ci-tracker/cards',
{
method: 'POST',
headers: {
'Authorization': 'Bearer ' + TACK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: `Test failure: ${testName}`,
body: [
`## Failed test\n\`${testName}\``,
`## Error\n\`\`\`\n${errorLog}\n\`\`\``,
`## Commit\n${commitSha}`,
].join('\n\n'),
list: 'backlog',
priority: 'high',
labels: ['ci-failure'],
metadata: { commit_sha: commitSha },
}),
}
).then(r => r.json());
return card;
}
// CI script: close a card when the fix ships
async function resolveCard(cardId, fixCommitSha) {
await fetch(
`/tack/v1/workspaces/acme-eng/projects/ci-tracker/cards/${cardId}/complete`,
{
method: 'POST',
headers: { 'Authorization': 'Bearer ' + TACK_API_KEY },
}
);
await fetch(
`/tack/v1/workspaces/acme-eng/projects/ci-tracker/cards/${cardId}/comments`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer ' + TACK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
body: `Fixed in commit ${fixCommitSha}. Card auto-completed by CI.`,
}),
}
);
}Multi-tenant project boards
Embed Tack boards in your SaaS product. Each customer gets their own Workspace, and Heimdall tokens enforce isolation automatically.
Steps
- 1When a customer signs up, create a Heimdall App and a Tack Workspace
- 2Map the Heimdall App to the Tack Workspace (store the workspace_id)
- 3Issue Heimdall tokens for the customer's users
- 4Use those tokens to authenticate Tack API calls — Tack validates the JWT and scopes access to the matching Workspace
- 5Customers can only see and modify their own projects, cards, and labels
Code example
// Customer onboarding: create workspace and default project
async function onboardCustomer(customerName, adminEmail) {
// 1. Create Tack workspace
const workspace = await fetch('/tack/v1/workspaces', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + ADMIN_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: slugify(customerName),
display_name: customerName,
metadata: { heimdall_app: slugify(customerName) },
}),
}).then(r => r.json());
// 2. Create a default project
await fetch(`/tack/v1/workspaces/${workspace.name}/projects`, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + ADMIN_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'main',
display_name: 'Main Board',
lists: [
{ name: 'to-do', display_name: 'To Do' },
{ name: 'in-progress', display_name: 'In Progress' },
{ name: 'done', display_name: 'Done' },
],
}),
});
// 3. Create default labels
const labels = ['feature', 'bug', 'improvement'];
for (const label of labels) {
await fetch(`/tack/v1/workspaces/${workspace.name}/labels`, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + ADMIN_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: label }),
});
}
return workspace;
}Webhook-driven automation
React to card events in real time. Trigger notifications, update external systems, or kick off workflows when cards move through your board.
Steps
- 1Register a webhook endpoint for the events you care about
- 2Tack sends signed POST requests to your endpoint on each matching event
- 3Verify the webhook signature using your signing secret
- 4Process the event payload and respond with a 200 status
- 5If your endpoint is unreachable, Tack retries with exponential backoff for up to 24 hours
Code example
// Webhook handler (Express example)
app.post('/webhooks/tack', async (req, res) => {
const signature = req.headers['x-tack-signature'];
const isValid = verifyHmac(
JSON.stringify(req.body),
signature,
process.env.TACK_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body;
switch (event.type) {
case 'card.created':
await notifySlackChannel(
`New card: ${event.data.title} in ${event.data.project_name}`
);
break;
case 'card.moved':
if (event.data.to_list === 'done') {
await updateExternalTracker(event.data.card_id, 'completed');
}
break;
case 'card.completed':
await sendCompletionEmail(
event.data.assignees,
event.data.title
);
break;
case 'comment.added':
await indexForSearch(event.data.card_id, event.data.body);
break;
}
res.status(200).json({ received: true });
});Batch operations for backlog grooming
Use the batch-move endpoint to reorganize your board in a single API call. Move, reprioritize, or relabel dozens of cards at once — ideal for weekly grooming sessions or automated triage.
Steps
- 1Fetch all cards in the target list
- 2Decide the new order, list placement, or priority for each card
- 3Send a single batch-move request with up to 100 card operations
- 4Tack applies all moves atomically — if one fails, none are applied
- 5Check the activity feed to confirm the changes
Code example
// Weekly grooming: move stale backlog items to an "icebox" list
async function groomBacklog(workspaceName, projectName) {
const thirtyDaysAgo = new Date(
Date.now() - 30 * 24 * 60 * 60 * 1000
).toISOString();
// Fetch all backlog cards
const cards = await fetch(
`/tack/v1/workspaces/${workspaceName}/projects/${projectName}/cards?list=backlog&completed=false`,
{ headers: { 'Authorization': 'Bearer ' + TACK_API_KEY } }
).then(r => r.json());
// Find stale cards (no updates in 30 days)
const staleCards = cards.data.filter(
card => card.updated_at < thirtyDaysAgo
);
if (staleCards.length === 0) return;
// Batch-move stale cards to icebox
await fetch(
`/tack/v1/workspaces/${workspaceName}/projects/${projectName}/cards/batch-move`,
{
method: 'POST',
headers: {
'Authorization': 'Bearer ' + TACK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
moves: staleCards.map((card, i) => ({
card_id: card.id,
list: 'icebox',
position: i,
})),
}),
}
);
console.log(`Moved ${staleCards.length} stale cards to icebox`);
}