End-to-End Walkthrough
This tutorial walks you through every major feature of Alloy using nothing but
curl and jq. By the end you will have created an organization, a project,
labels, a sprint, tickets, comments, and time entries — the full lifecycle of a
project management workflow.
1. Prerequisites
You need:
- A running Alloy server (see Getting Started)
curlandjqinstalled- A terminal with shell variable support
Start the server with open registration:
ALLOY_REGISTRATION=open ./target/release/alloy serve
Set the base URL in your terminal:
BASE_URL="http://localhost:3000"
2. Register and Authenticate
Create a user account and capture the access token:
curl -s -X POST "$BASE_URL/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d '{
"email": "walkthrough@alloy.dev",
"password": "walkthrough1",
"display_name": "Walkthrough User"
}' | jq .
{
"user_id": "...",
"email": "walkthrough@alloy.dev",
"display_name": "Walkthrough User",
"access_token": "..."
}
Save the token and user ID for subsequent requests:
TOKEN="<access_token from above>"
USER_ID="<user_id from above>"
CLI shortcut:
alloy auth login --email walkthrough@alloy.dev --password walkthrough1
3. Create an Organization
Every project lives inside an organization. Create one now:
curl -s -X POST "$BASE_URL/api/v1/orgs" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "Acme Corp",
"slug": "acme-wt"
}' | jq .
{
"id": "...",
"name": "Acme Corp",
"slug": "acme-wt"
}
Save the org ID:
ORG_ID="<id from above>"
Verify it by listing your organizations:
curl -s "$BASE_URL/api/v1/orgs" \
-H "Authorization: Bearer $TOKEN" | jq .
{
"items": [
{
"id": "...",
"name": "...",
"slug": "..."
}
]
}
CLI shortcut:
alloy org create --name "Acme Corp" --slug acme-wt
4. Create a Project
Projects group tickets, sprints, and labels under a short key used in ticket
references (e.g. ALLOY-1):
curl -s -X POST "$BASE_URL/api/v1/projects" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"org_id\": \"$ORG_ID\",
\"key\": \"ALLOY\",
\"name\": \"Alloy Project\",
\"description\": \"End-to-end walkthrough project\"
}" | jq .
{
"id": "...",
"org_id": "...",
"key": "ALLOY",
"name": "Alloy Project",
"description": "End-to-end walkthrough project"
}
Save the project ID:
PROJECT_ID="<id from above>"
Read it back to confirm:
curl -s "$BASE_URL/api/v1/projects/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" | jq .
{
"id": "...",
"key": "ALLOY",
"name": "Alloy Project",
"description": "End-to-end walkthrough project"
}
CLI shortcut:
alloy project create --org-id "$ORG_ID" --key ALLOY --name "Alloy Project"
5. Create Labels
Labels categorize tickets. Create four labels for your organization:
curl -s -X POST "$BASE_URL/api/v1/orgs/$ORG_ID/labels" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "bug",
"color": "#FF0000"
}' | jq .
{
"id": "...",
"name": "bug",
"color": "#FF0000"
}
curl -s -X POST "$BASE_URL/api/v1/orgs/$ORG_ID/labels" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "feature",
"color": "#00FF00"
}' | jq .
{
"id": "...",
"name": "feature",
"color": "#00FF00"
}
curl -s -X POST "$BASE_URL/api/v1/orgs/$ORG_ID/labels" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "docs",
"color": "#0000FF"
}' | jq .
{
"id": "...",
"name": "docs",
"color": "#0000FF"
}
curl -s -X POST "$BASE_URL/api/v1/orgs/$ORG_ID/labels" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "urgent",
"color": "#FFA500"
}' | jq .
{
"id": "...",
"name": "urgent",
"color": "#FFA500"
}
List all labels in the org to confirm:
curl -s "$BASE_URL/api/v1/orgs/$ORG_ID/labels" \
-H "Authorization: Bearer $TOKEN" | jq .
{
"items": [
{
"id": "...",
"name": "...",
"color": "..."
}
],
"next_cursor": null,
"has_more": false
}
CLI shortcut:
alloy label create --org-id "$ORG_ID" --name bug --color "#FF0000" alloy label list --org-id "$ORG_ID"
6. Create a Sprint
Sprints are time-boxed iterations. Create one for your project:
curl -s -X POST "$BASE_URL/api/v1/projects/$PROJECT_ID/sprints" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "Sprint 1",
"goal": "Complete the walkthrough",
"start_date": "2026-03-28",
"end_date": "2026-04-11"
}' | jq .
{
"id": "...",
"name": "Sprint 1",
"goal": "Complete the walkthrough",
"start_date": "2026-03-28",
"end_date": "2026-04-11"
}
Save the sprint ID:
SPRINT_ID="<id from above>"
Read it back:
curl -s "$BASE_URL/api/v1/sprints/$SPRINT_ID" \
-H "Authorization: Bearer $TOKEN" | jq .
{
"id": "...",
"name": "Sprint 1",
"goal": "Complete the walkthrough"
}
CLI shortcut:
alloy sprint create --project-id "$PROJECT_ID" --name "Sprint 1" --goal "Complete the walkthrough"
7. Create Tickets
Tickets are the core work items. Create a ticket in the project:
curl -s -X POST "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"title\": \"Set up CI pipeline\",
\"description\": \"Configure GitHub Actions for build and test\",
\"status\": \"Backlog\",
\"priority\": \"High\",
\"reporter_id\": \"$USER_ID\"
}" | jq .
{
"id": "...",
"title": "Set up CI pipeline",
"description": "Configure GitHub Actions for build and test",
"status": "Backlog",
"priority": "High",
"reporter_id": "..."
}
Save the ticket ID:
TICKET_ID="<id from above>"
Create a second ticket:
curl -s -X POST "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"title\": \"Fix login redirect\",
\"description\": \"Users are redirected to the wrong page after login\",
\"status\": \"InProgress\",
\"priority\": \"High\",
\"reporter_id\": \"$USER_ID\"
}" | jq .
{
"id": "...",
"title": "Fix login redirect",
"description": "Users are redirected to the wrong page after login",
"status": "InProgress",
"priority": "High",
"reporter_id": "..."
}
And a third:
curl -s -X POST "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"title\": \"Write API docs\",
\"description\": \"Document all REST endpoints\",
\"status\": \"Backlog\",
\"priority\": \"Low\",
\"reporter_id\": \"$USER_ID\"
}" | jq .
{
"id": "...",
"title": "Write API docs",
"description": "Document all REST endpoints",
"status": "Backlog",
"priority": "Low",
"reporter_id": "..."
}
CLI shortcut:
alloy ticket create --title "Set up CI pipeline" --priority high alloy ticket create --title "Fix login redirect" --status InProgress --priority high
8. Assign Labels to Tickets
Attach a label to a ticket using the set-labels endpoint:
curl -s -X POST "$BASE_URL/api/v1/tickets/$TICKET_ID/labels" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"label_ids\": [\"$LABEL_ID\"]
}" | jq .
[
{
"id": "...",
"name": "...",
"color": "..."
}
]
List labels on the ticket to verify:
curl -s "$BASE_URL/api/v1/tickets/$TICKET_ID/labels" \
-H "Authorization: Bearer $TOKEN" | jq .
[
{
"id": "...",
"name": "...",
"color": "..."
}
]
CLI shortcut:
alloy label set --ticket-id "$TICKET_ID" --label-ids "$LABEL_ID"
9. Transition Ticket Status
Move a ticket through workflow states. First check available transitions:
curl -s "$BASE_URL/api/v1/tickets/$TICKET_ID/transitions" \
-H "Authorization: Bearer $TOKEN" | jq .
{
"current_status": "...",
"available": [
{
"name": "...",
"category": "..."
}
]
}
Transition the ticket to “InProgress”:
curl -s -X POST "$BASE_URL/api/v1/tickets/$TICKET_ID/transition" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"to_status": "InProgress"
}' | jq .
{
"id": "...",
"title": "...",
"status": "InProgress"
}
CLI shortcut:
alloy ticket transition --id "$TICKET_ID" --status InProgress
10. Add Comments
Add discussion to a ticket:
curl -s -X POST "$BASE_URL/api/v1/tickets/$TICKET_ID/comments" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"body\": \"Looks good, let's ship it\",
\"author_id\": \"$USER_ID\"
}" | jq .
{
"id": "...",
"body": "Looks good, let's ship it",
"author_id": "...",
"ticket_id": "..."
}
List comments on the ticket:
curl -s "$BASE_URL/api/v1/tickets/$TICKET_ID/comments" \
-H "Authorization: Bearer $TOKEN" | jq .
{
"items": [
{
"id": "...",
"body": "...",
"author_id": "...",
"ticket_id": "..."
}
],
"next_cursor": null,
"has_more": false
}
CLI shortcut:
alloy comment create --ticket-id "$TICKET_ID" --body "Looks good, let's ship it" alloy comment list --ticket-id "$TICKET_ID"
11. Track Time
Log time spent working on a ticket:
curl -s -X POST "$BASE_URL/api/v1/time-entries" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"user_id\": \"$USER_ID\",
\"ticket_id\": \"$TICKET_ID\",
\"project_id\": \"$PROJECT_ID\",
\"date\": \"2026-03-28\",
\"duration_minutes\": 120,
\"description\": \"Initial setup work\",
\"activity_type\": \"Coding\"
}" | jq .
{
"id": "...",
"user_id": "...",
"ticket_id": "...",
"project_id": "...",
"date": "2026-03-28",
"duration_minutes": 120,
"description": "Initial setup work",
"activity_type": "Coding"
}
List time entries for the ticket:
curl -s "$BASE_URL/api/v1/tickets/$TICKET_ID/time-entries" \
-H "Authorization: Bearer $TOKEN" | jq .
{
"items": [
{
"id": "...",
"duration_minutes": 120,
"description": "Initial setup work",
"activity_type": "Coding"
}
],
"next_cursor": null,
"has_more": false
}
CLI shortcut:
alloy time log --ticket-id "$TICKET_ID" --duration 120 --description "Initial setup work" --activity-type Coding alloy time list --ticket-id "$TICKET_ID"
12. List and Filter Tickets
List all tickets in the project:
curl -s "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets" \
-H "Authorization: Bearer $TOKEN" | jq .
{
"items": [
{
"id": "...",
"title": "...",
"status": "...",
"priority": "..."
}
],
"next_cursor": null,
"has_more": false
}
Filter by status:
curl -s "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets?status=InProgress" \
-H "Authorization: Bearer $TOKEN" | jq .
{
"items": [
{
"id": "...",
"title": "...",
"status": "InProgress",
"priority": "..."
}
],
"next_cursor": null,
"has_more": false
}
CLI shortcut:
alloy ticket list --project-id "$PROJECT_ID" alloy ticket list --project-id "$PROJECT_ID" --status InProgress
13. Finance: Tags, Budgets, and Capitalization Reports
Alloy supports entity tagging, project budgets, and detailed capitalization reporting for software cost accounting.
Tag a Project
Tags are key-value pairs you can attach to projects, tickets, users, teams, or time entries:
curl -s -X PUT "$BASE_URL/api/v1/orgs/$ORG_ID/project/$PROJECT_ID/tags" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"tags": [
{"key": "department", "value": "engineering"},
{"key": "cost-type", "value": "capex"}
]
}' | jq .
[
{
"id": "...",
"entity_type": "project",
"entity_id": "...",
"key": "department",
"value": "engineering",
"created_at": "...",
"updated_at": "..."
}
]
Read back tags:
curl -s "$BASE_URL/api/v1/orgs/$ORG_ID/project/$PROJECT_ID/tags" \
-H "Authorization: Bearer $TOKEN" | jq .
Search for all entities with a given tag:
curl -s "$BASE_URL/api/v1/orgs/$ORG_ID/tags/search?key=department&value=engineering" \
-H "Authorization: Bearer $TOKEN" | jq .
Set a Project Budget
Update a project with budget tracking fields:
curl -s -X PATCH "$BASE_URL/api/v1/projects/$PROJECT_ID" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"budget_cents": 1000000,
"budget_period": "Monthly"
}' | jq .
{
"id": "...",
"name": "Alloy Project",
"budget_cents": 1000000,
"budget_period": "Monthly"
}
Supported budget periods: Monthly, Quarterly, Yearly, Fixed.
Run a Capitalization Report
Generate a report for a given month:
curl -s "$BASE_URL/api/v1/reports/capitalization?period=2026-03" \
-H "Authorization: Bearer $TOKEN" | jq .
Add budget/ROI data with include_budget=true:
curl -s "$BASE_URL/api/v1/reports/capitalization?period=2026-03&include_budget=true" \
-H "Authorization: Bearer $TOKEN" | jq .
Group by team or user:
curl -s "$BASE_URL/api/v1/reports/capitalization?period=2026-03&group_by=team" \
-H "Authorization: Bearer $TOKEN" | jq .
Filter by tag:
curl -s "$BASE_URL/api/v1/reports/capitalization?period=2026-03&tag=department:engineering" \
-H "Authorization: Bearer $TOKEN" | jq .
Export as CSV:
curl -s "$BASE_URL/api/v1/reports/capitalization/export?period=2026-03" \
-H "Authorization: Bearer $TOKEN" -o report.csv
See the API Reference — Entity Tags and Capitalization Reports sections for the full list of query parameters.
14. Custom Workflows
Alloy supports custom workflows with named statuses, allowed transitions, and enforcement levels. Create a strict workflow that enforces a Kanban-style flow:
curl -s -X POST "$BASE_URL/api/v1/orgs/$ORG_ID/workflows" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "Kanban",
"statuses": [
{"name": "Open", "category": "todo"},
{"name": "Coding", "category": "in_progress"},
{"name": "Review", "category": "in_progress"},
{"name": "Shipped", "category": "done"}
],
"transitions": [
{"from": "Open", "to": "Coding"},
{"from": "Coding", "to": "Review"},
{"from": "Review", "to": "Shipped"}
],
"enforcement": "strict"
}' | jq .
Expected response (201 Created):
{
"id": "...",
"org_id": "...",
"name": "Kanban",
"statuses": [
{"name": "Open", "category": "todo"},
{"name": "Coding", "category": "in_progress"},
{"name": "Review", "category": "in_progress"},
{"name": "Shipped", "category": "done"}
],
"transitions": [
{"from": "Open", "to": "Coding"},
{"from": "Coding", "to": "Review"},
{"from": "Review", "to": "Shipped"}
],
"enforcement": "strict",
"created_at": "...",
"updated_at": "..."
}
Save the workflow ID:
WORKFLOW_ID="<id from above>"
Workflow Enforcement in Action
Create a ticket using a custom workflow status and test the transition rules. First, create a project that uses the default workflow, then update the default workflow to strict enforcement:
curl -s -X POST "$BASE_URL/api/v1/projects" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"org_id\": \"$ORG_ID\",
\"key\": \"WFT\",
\"name\": \"Workflow Test Project\"
}" | jq .
Expected response (201 Created):
{
"id": "...",
"org_id": "...",
"key": "WFT",
"name": "Workflow Test Project"
}
Create a ticket with the custom status “Open”:
curl -s -X POST "$BASE_URL/api/v1/projects/$WF_PROJECT_ID/tickets" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"title\": \"Workflow test ticket\",
\"description\": \"Testing enforcement\",
\"status\": \"Open\",
\"priority\": \"Medium\",
\"reporter_id\": \"$USER_ID\"
}" | jq .
Expected response (201 Created):
{
"id": "...",
"title": "Workflow test ticket",
"status": "Open",
"priority": "Medium"
}
A valid transition (Open → Coding) succeeds:
curl -s -X POST "$BASE_URL/api/v1/tickets/$WF_TICKET_ID/transition" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"to_status": "Coding"}' | jq .
Expected response (200 OK):
{
"id": "...",
"title": "Workflow test ticket",
"status": "Coding"
}
An invalid transition (Coding → Shipped, skipping Review) is rejected with a 422 error under strict enforcement:
curl -s -X POST "$BASE_URL/api/v1/tickets/$WF_TICKET_ID/transition" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"to_status": "Shipped"}' | jq .
Expected response (422 Unprocessable Entity):
{
"error": {
"code": "VALIDATION_ERROR",
"message": "transition from 'Coding' to 'Shipped' is not allowed by workflow 'Kanban'",
"details": []
}
}
Enforcement modes: none (any transition allowed), warn (invalid transitions
succeed with a warning), strict (invalid transitions rejected with 422).
15. Delete Guards
Alloy protects resources that have dependents from accidental deletion. Trying
to delete a project that still has tickets returns 409 Conflict:
curl -s -X DELETE "$BASE_URL/api/v1/projects/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" | jq .
Expected response (409 Conflict):
{
"error": {
"code": "CONFLICT",
"message": "Cannot delete project with existing dependents",
"details": ["tickets: 3", "sprints: 1"]
}
}
The same applies to tickets with comments:
curl -s -X DELETE "$BASE_URL/api/v1/tickets/$TICKET_ID" \
-H "Authorization: Bearer $TOKEN" | jq .
Expected response (409 Conflict):
{
"error": {
"code": "CONFLICT",
"message": "Cannot delete ticket with existing dependents",
"details": ["comments: 1"]
}
}
Cascade Dry-Run Preview
Before committing to a cascade delete, you can preview what would be removed
using cascade=true&dry_run=true. This returns a 200 response listing
dependents without deleting anything:
curl -s -X DELETE \
"$BASE_URL/api/v1/tickets/$TICKET_ID?cascade=true&dry_run=true" \
-H "Authorization: Bearer $TOKEN" | jq .
Expected response (200 OK — nothing is deleted):
{
"ticket_id": "...",
"dependents": {
"comments": 1
}
}
Preview a project cascade to see all nested dependents:
curl -s -X DELETE \
"$BASE_URL/api/v1/projects/$PROJECT_ID?cascade=true&dry_run=true" \
-H "Authorization: Bearer $TOKEN" | jq .
Expected response (200 OK — nothing is deleted):
{
"project_id": "...",
"dependents": {
"tickets": 3,
"sprints": 1,
"comments": 1
}
}
To actually perform the cascade delete (removes the resource and all its
dependents), use cascade=true without dry_run:
curl -s -X DELETE \
"$BASE_URL/api/v1/tickets/$TICKET_ID?cascade=true" \
-H "Authorization: Bearer $TOKEN"
# Returns: 204 No Content
16. Roles and Permissions
Alloy uses a role hierarchy: Owner > Admin > Member > Reporter > Viewer. Different roles have different permissions. Here we demonstrate the difference between a Reporter and a Member.
Invite a Reporter
Create an invite with the Reporter role and register a new user with it:
curl -s -X POST "$BASE_URL/api/v1/orgs/$ORG_ID/invites" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"email\": \"reporter@alloy.dev\",
\"role\": \"reporter\",
\"created_by\": \"$USER_ID\"
}" | jq .
Expected response (201 Created):
{
"id": "...",
"email": "reporter@alloy.dev",
"role": "Reporter",
"invite_code": "..."
}
INVITE_CODE="<invite_code from above>"
Register the reporter using the invite code:
curl -s -X POST "$BASE_URL/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"reporter@alloy.dev\",
\"password\": \"repreprep1\",
\"display_name\": \"Reporter User\",
\"invite_code\": \"$INVITE_CODE\"
}" | jq .
Expected response (201 Created):
{
"user_id": "...",
"email": "reporter@alloy.dev",
"display_name": "Reporter User",
"access_token": "..."
}
REPORTER_TOKEN="<access_token from above>"
REPORTER_USER_ID="<user_id from above>"
Reporter Can Create Tickets
Reporters can create tickets in projects they are assigned to:
curl -s -X POST "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $REPORTER_TOKEN" \
-d "{
\"title\": \"Reporter ticket\",
\"description\": \"Created by reporter\",
\"status\": \"Backlog\",
\"priority\": \"Low\",
\"reporter_id\": \"$REPORTER_USER_ID\"
}" | jq .
Expected response (201 Created):
{
"id": "...",
"title": "Reporter ticket",
"description": "Created by reporter",
"status": "Backlog",
"priority": "Low",
"reporter_id": "..."
}
Reporter Cannot Update Tickets
Updating tickets requires the Member role or above. A Reporter gets 403 Forbidden:
curl -s -X PATCH "$BASE_URL/api/v1/tickets/$REPORTER_TICKET_ID" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $REPORTER_TOKEN" \
-d '{"title": "Updated by reporter"}' | jq .
Expected response (403 Forbidden):
{
"error": {
"code": "FORBIDDEN",
"message": "requires Member role or above"
}
}
Reporter Cannot Create Projects
Creating projects also requires the Member role or above:
curl -s -X POST "$BASE_URL/api/v1/projects" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $REPORTER_TOKEN" \
-d "{
\"org_id\": \"$ORG_ID\",
\"key\": \"REP1\",
\"name\": \"Reporter Project\"
}" | jq .
Expected response (403 Forbidden):
{
"error": {
"code": "FORBIDDEN",
"message": "requires Member role or above"
}
}
See the Getting Started guide for the full permission matrix showing what each role can do.
17. Next Steps
You have now used every core feature of Alloy through the API. Here is what to explore next:
- API Reference — Full endpoint documentation including workflows, webhooks, SCIM provisioning, and integrations
- CLI Reference — Complete command-line interface for scripting and automation
- TUI Guide — Interactive terminal UI for browsing and managing work visually
- Deployment — Production deployment with PostgreSQL, multi-tenancy, and SSO
To seed a full demo dataset automatically:
BASE_URL=http://localhost:3000 bash scripts/seed-demo.sh