Project management
A workspace can contain many nested boards and cards, but ticket numbers may need to span the whole workspace. This example combines local ranks with that wider counter and shows how to return to cards to add a sibling branch.
Workspace → Project → Board → Column → Card → ChecklistItem, with Comment as a sibling branch of the checklist. Ranks restart per parent, but ticket numbers span the whole workspace. Source: tests/Zeeq.Biome.Tests/ProjectManagement.
Rank scopes and a workspace ticket counter
// A new column restarts card ranks while Under(column) resumes its progression.
internal static readonly Sequence<string> CardRanks = Sequence
.Of("a", r => r + "a")
.PerParent();
// Ticket numbers span columns/boards/projects,
// reaching the nearest workspace ancestor.
internal static readonly Sequence<int> Tickets = Sequence
.Of(1, n => n + 1)
.Per<Workspace>();
// A deep ancestor supplies tenancy; a Derive
// chain supplies a title from the ticket.
internal static readonly ChildBlueprint<ProjectSeed, Column, Card> Card =
Blueprint
.ChildOf<ProjectSeed, Column, Card>(
(column, ctx) =>
new()
{
Column = column,
Workspace = ctx.Ancestor<Workspace>(),
DueAt = ctx.FromNow(TimeSpan.FromDays(1)),
}
)
.Derive(c => c.Rank, CardRanks)
.Derive(c => c.Ticket, Tickets)
.Derive(c => c.Title, (c, _) => $"Ticket {c.Ticket}");
ctx.Ancestor<Workspace>() reaches five edges up from a card's column. There's no runtime ancestor auto-creation: the chain must already contain a workspace, and the error says so when it doesn't.A portfolio, then sibling comments
Each count multiplies the previous output. After the deepest leaves, .Under(cards) jumps back to attach comments as siblings of the checklist.
await biome
.AddWorkspaces(out var workspaces, 2)
.AddProjects(out _, 2) // 4 projects
.AddBoards(out _, 1)
.AddColumns(out var columns, 2) // 8 columns
.AddCards(out var cards, 3) // 24 cards
.AddChecklistItems(out var checklist, 4) // 96 deepest leaves
.Under(cards) // comments attach to cards, not checklist items
.AddComments(out var comments, 2)
.BuildAsync();
var now = DateTimeOffset.UtcNow;
var workspaces = new[]
{
new Workspace { Owner = "owner" },
new Workspace { Owner = "owner" },
};
var projects = workspaces
.SelectMany(w =>
Enumerable.Range(0, 2).Select(_ => new Project { Workspace = w })
)
.ToList();
var boards = projects.Select(p => new Board { Project = p }).ToList();
var columns = new List<Column>();
var cards = new List<Card>();
var checklist = new List<ChecklistItem>();
var comments = new List<Comment>();
var ticket = workspaces.ToDictionary(w => w, _ => 1); // per workspace, by hand
foreach (var board in boards)
{
var workspace = board.Project.Workspace;
var columnRank = "a";
foreach (var _ in Enumerable.Range(0, 2))
{
var column = new Column
{
Board = board,
Rank = columnRank,
Name = $"Column {columnRank}",
};
columnRank += "a";
columns.Add(column);
var cardRank = "a"; // restarts per column
foreach (var __ in Enumerable.Range(0, 3))
{
var card = new Card
{
Column = column,
Workspace = workspace,
Rank = cardRank,
Ticket = ticket[workspace]++,
DueAt = now.AddDays(1),
};
card.Title = $"Ticket {card.Ticket}";
cardRank += "a";
cards.Add(card);
var itemRank = "a"; // restarts per card
foreach (var ___ in Enumerable.Range(0, 4))
{
checklist.Add(
new ChecklistItem
{
Card = card,
Workspace = workspace,
Rank = itemRank,
Text = $"Task {itemRank}",
}
);
itemRank += "a";
}
for (var c = 1; c <= 2; c++)
{
comments.Add(
new Comment
{
Card = card,
Workspace = workspace,
Number = c,
Text = $"Comment {c}",
CreatedAt = now.AddHours(-1),
}
);
}
}
}
}
db.AddRange(workspaces);
db.AddRange(projects);
db.AddRange(boards);
db.AddRange(columns);
db.AddRange(cards);
db.AddRange(checklist);
db.AddRange(comments);
await db.SaveChangesAsync();
The real test checks that tenancy flows through every level (checklist[95].Workspace == workspaces[1]), that ranks restart (columns[2].Rank == "a", cards[3].Rank == "a"), and that all 24 × 2 comments share one frozen timestamp.
Local ranks skip explicit values; tickets span columns
await biome
.AddWorkspace(out _)
.AddProject(out _)
.AddBoard(out _)
.AddColumns(out var columns, 2)
.AddCards(
out var cards,
Cards.Blocked + Cards.Urgent,
// Explicit "a" is reserved before the first card
// is filled; its rank becomes "aa".
c =>
{
c.Rank = "a";
c.Title = "Regression";
}
)
.AddChecklistItems(out _, 2)
// Resume one column's rank and the encompassing workspace's ticket sequence.
.Under(columns[0])
.AddCard(out var next)
.BuildAsync();
// cards[0].Rank == "aa"; cards[2].Ticket == 3
// next.Rank == "aaa"; next.Ticket == 5; next.Title == "Ticket 5"
Cards.Blocked + Cards.Urgent is one card with both traits, while the second customizer is the second card in each column. Four cards total, each column ranking its own.A ready-made six-level branch, with a transient comment
var time = new FakeTimeProvider(
new DateTimeOffset(2026, 10, 11, 12, 0, 0, TimeSpan.Zero)
);
var biome = TestBiome.Start(out _, store, time);
biome.AddReadyChecklist(out var leaf);
time.Advance(TimeSpan.FromHours(2));
await biome
.Under(leaf.Card)
.AddComment(out var preview)
// Built and returned, but NOT saved: this models a draft comment request.
.Transient()
.BuildAsync();
// store.Entities.Count == 6; no Comment saved
// preview.CreatedAt == time.GetUtcNow().AddHours(-1)