For revenue engineers
Shape your customer data at agent speed
Brindle gives revenue engineers the building blocks for a customer system that fits how the business sells, and reshapes itself as the business changes.
Data model
A graph of four CRM objects, account, contact, deal and task, with their records round them; a fifth object grows on the right and a link runs to it from the account, then records appear on it.
702.0) return 0.0;
float ph = TAU * t / T;
float v = 0.0;
// the preview: grows 0.9 to 1.7 s, discarded 5.0 to 5.8 s
float env = ease(sm(0.9, 1.7, t)) * (1.0 - ease(sm(5.0, 5.8, t)));
// the four objects and their links
float ln = 0.0;
ln = max(ln, lk(p, vec2(285.0, 215.0), vec2(100.0, 125.0), 0.4));
ln = max(ln, lk(p, vec2(285.0, 215.0), vec2(190.0, 352.0), 0.4));
ln = max(ln, lk(p, vec2(285.0, 215.0), vec2(470.0, 105.0), 0.4));
ln = max(ln, lk(p, vec2(100.0, 125.0), vec2(190.0, 352.0), 0.4));
ln = max(ln, lk(p, vec2(470.0, 105.0), vec2(190.0, 352.0), 0.4));
v += ln;
v += hubf(p, vec2(285.0, 215.0), 44.0, 0.0, ph);
v += hosts(p, vec2(285.0, 215.0), 44.0, 1.0, ph, 8.0);
v += hubf(p, vec2(100.0, 125.0), 30.0, 1.0, ph);
v += hosts(p, vec2(100.0, 125.0), 30.0, 2.0, ph, 8.0);
v += hubf(p, vec2(190.0, 352.0), 36.0, 2.0, ph);
v += hosts(p, vec2(190.0, 352.0), 36.0, 3.0, ph, 8.0);
v += hubf(p, vec2(470.0, 105.0), 28.0, 3.0, ph);
v += hosts(p, vec2(470.0, 105.0), 28.0, 4.0, ph, 8.0);
// RENEWAL: declared, then grown from a point; its ring goes out as it settles
vec2 N = vec2(585.0, 228.0);
v += env * hubf(p, N, 38.0 * (0.35 + 0.65 * ease(sm(0.9, 1.7, t))), 7.0, ph);
float g = t - 1.3;
if (g > 0.0 && g < 0.7) v += 0.55 * (1.0 - g / 0.7) * exp(-pow(length((p - N) * vec2(1.0, 1.2)) - 30.0 - 60.0 * g, 2.0) / 28.0);
// the link and its packet: the packet leaves ACCOUNT at 1.9 s and arrives at 2.9 s; the link lights on arrival
{
float u = clamp((t - 1.9) / 1.0, 0.0, 1.0);
vec2 A = vec2(285.0, 215.0);
vec2 c = mix(A, N, ease(u));
float vis = (1.0 - ease(sm(5.0, 5.8, t)));
if (u > 0.0) {
v += vis * lk(p, A, c, 0.62);
if (u < 1.0) v += blob(p, c, 8.0, 1.35);
v += vis * sm(0.97, 1.0, u) * blob(p, N, 30.4, 0.4);
}
float a2 = t - 2.9;
if (a2 > 0.0 && a2 < 0.4) v += 0.6 * (1.0 - a2 / 0.4) * exp(-pow(length(p - N) - 36.0 - 70.0 * a2, 2.0) / 24.0);
}
// records appear on RENEWAL one by one from 3.0 s
v += env * hosts(p, N, 38.0, 8.0, ph, 8.0 * ease(sm(3.0, 4.4, t)));
// the strip: empty slots, then a bar per step once it is done
if (p.x > 436.0 && p.x < 574.0 && p.y > 348.0 && p.y < 421.0) {
float slot = mod(p.x - 441.0, 44.0);
v += slot < 8.0 ? 0.13 : 0.0;
}
v += bar(p, 441.0, 386.0, sm(1.65, 1.75, t) * (1.0 - sm(5.0, 5.8, t)));
v += bar(p, 485.0, 368.0, sm(2.85, 2.9499999999999997, t) * (1.0 - sm(5.0, 5.8, t)));
v += bar(p, 529.0, 350.0, sm(4.3500000000000005, 4.45, t) * (1.0 - sm(5.0, 5.8, t)));
return v;
}]]>
Open-source core Read every line and run it on your own servers.
Live in an hour A first schema and real records with the local setup.
Agent ready A schema and command reference your agents can read.
The problem
A custom system gives your team an edge, but building one comes with tradeoffs
Tradeoffs
On the left a wide slab of five sealed layers that a change has to step down one at a time; on the right six small in-house services joined by thin links, where one link fails and stalls the change until it is mended.
702.0) return 0.0;
float ph = TAU * t / T;
float v = 0.0;
float vis = 1.0 - ease(sm(6.2, 6.9, t));
// the monolith: five ribbed layers in a slab with a bright rim
float s = clamp((t - 0.4) / 5.0, 0.0, 1.0) * 4.0;
float kk = min(floor(s), 3.0);
float py = 128.0 + 58.0 * (kk + ease(s - kk));
for (int i = 0; i < 5; i++) {
float fi = float(i);
float cy = 128.0 + 58.0 * fi;
float inb = step(abs(p.y - cy), 22.0) * step(40.0, p.x) * step(p.x, 290.0);
float rib = 0.5 + 0.5 * step(0.5, fract((p.x - 40.0) / 28.0));
float lit = vis * sm(fi - 0.08, fi, s) * step(0.0, t - 0.4);
float rim = step(abs(abs(p.y - cy) - 22.0), 3.5) * step(40.0, p.x) * step(p.x, 290.0);
v += inb * (0.34 * rib + 0.5 * lit * (0.7 + 0.3 * noise(p * 0.07 + fi))) + 0.3 * rim * (0.4 + lit);
}
v += step(40.0, p.x) * step(p.x, 290.0) * step(abs(p.y - 244.0), 150.0) * 0.1 * (0.6 + 0.4 * sin(ph + p.x * 0.05));
// the change request: a dense packet stepping down through the layers
float pk = step(0.4, t) * step(t, 5.5) * (1.0 - ease(sm(5.3, 5.5, t)));
v += pk * blob(p, vec2(165.0, py), 10.0, 1.3);
// the in-house services and their links
float fail = 1.0 - sm(1.6, 1.7, t) * (1.0 - sm(4.2, 4.4, t));
fail *= t > 1.6 && t < 1.9 ? 0.55 + 0.45 * sin(t * 140.0) : 1.0;
float ln = 0.0;
ln = max(ln, lk(p, vec2(410.0, 120.0), vec2(505.0, 200.0), 0.27));
ln = max(ln, lk(p, vec2(505.0, 200.0), vec2(610.0, 135.0), 0.27));
ln = max(ln, lk(p, vec2(505.0, 200.0), vec2(495.0, 350.0), 0.27));
ln = max(ln, lk(p, vec2(505.0, 200.0), vec2(615.0, 305.0), 0.27) * fail);
ln = max(ln, lk(p, vec2(615.0, 305.0), vec2(495.0, 350.0), 0.27));
ln = max(ln, lk(p, vec2(405.0, 260.0), vec2(505.0, 200.0), 0.27));
ln = max(ln, lk(p, vec2(405.0, 260.0), vec2(410.0, 120.0), 0.27));
ln = max(ln, lk(p, vec2(405.0, 260.0), vec2(495.0, 350.0), 0.27));
ln = max(ln, lk(p, vec2(610.0, 135.0), vec2(615.0, 305.0), 0.27));
v += ln;
vec2 mid = (vec2(505.0, 200.0) + vec2(615.0, 305.0)) * 0.5;
v += blob(p, mid, 11.0, 1.2 * exp(-pow(t - 1.72, 2.0) / 0.006));
v += blob(p, mid, 11.0, 1.1 * exp(-pow(t - 4.3, 2.0) / 0.012));
v += hubf(p, vec2(410.0, 120.0), 21.0, 3.0, ph);
v += hubf(p, vec2(505.0, 200.0), 24.0, 4.0, ph);
v += hubf(p, vec2(610.0, 135.0), 20.0, 5.0, ph);
v += hubf(p, vec2(615.0, 305.0), 24.0, 6.0, ph);
v += hubf(p, vec2(495.0, 350.0), 21.0, 7.0, ph);
v += hubf(p, vec2(405.0, 260.0), 18.0, 8.0, ph);
// the in-house change: R0 to R1, stalls at the failed link, then R1 to R3 to R4 once it is mended
{
float a = clamp((t - 0.4) / 1.0, 0.0, 1.0);
vec2 c0 = mix(vec2(410.0, 120.0), vec2(505.0, 200.0), ease(a));
float on0 = step(0.4, t) * (1.0 - ease(sm(1.5, 1.8, t))) * vis;
v += on0 * (blob(p, c0, 8.0, 1.3) + lk(p, vec2(410.0, 120.0), c0, 0.55));
float b = clamp((t - 4.4) / 0.8, 0.0, 1.0);
vec2 c1 = mix(vec2(505.0, 200.0), vec2(615.0, 305.0), ease(b));
float on1 = step(4.4, t) * vis * (1.0 - step(5.2, t));
v += on1 * (blob(p, c1, 8.0, 1.3) + lk(p, vec2(505.0, 200.0), c1, 0.6));
float c = clamp((t - 5.2) / 0.6, 0.0, 1.0);
vec2 c2 = mix(vec2(615.0, 305.0), vec2(495.0, 350.0), ease(c));
float on2 = step(5.2, t) * vis;
v += on2 * (blob(p, c2, 8.0, 1.3) + lk(p, vec2(615.0, 305.0), c2, 0.6) + lk(p, vec2(505.0, 200.0), vec2(615.0, 305.0), 0.6));
v += vis * step(1.4, t) * (1.0 - step(1.8, t)) * 0.0;
}
return v;
}]]>
The sealed monolith
Closed rule languages, release trains measured in quarters, and logic nobody can open.
The homegrown patchwork
It is brittle. The first version ships fast, but every change after that is a slow tax on the team that wrote it.
Stop settling for trade-offs
Assemble, reshape and extend a sturdy customer system that bends without breaking
Compose your pipeline and internal tools with a single set of building blocks.
Building blocks
Nine ribbed blocks are laid into three columns, data model, layout and logic, counted on a strip below; then a record runs from the first block to the second to the workflow block and the run is confirmed.
702.0) return 0.0;
float ph = TAU * t / T;
float s = mod(t + 5.2, T);
float v = 0.0;
float clear = 0.0;
for (int i = 0; i < 9; i++) {
float fi = float(i);
float c = floor(fi / 3.0);
float r = fi - 3.0 * c;
float x0 = 50.0 + 235.0 * c;
float y0 = 118.0 + 74.0 * r;
float t0 = 0.3 + 0.26 * fi;
float pr = ease(sm(t0, t0 + 0.5, s)) * (1.0 - ease(sm(5.6 + 0.05 * fi, 6.1 + 0.05 * fi, s)));
// the empty slot: a faint outline that is always there
float rimS = step(abs(abs(p.x - x0 - 75.0) - 75.0), 3.5) * step(abs(p.y - y0 - 26.0), 26.0) + step(abs(abs(p.y - y0 - 26.0) - 26.0), 3.5) * step(abs(p.x - x0 - 75.0), 75.0);
v += rimS * 0.17;
// the slab, risen from below by 36 px as it lands
vec2 q = p - vec2(x0, y0 + 36.0 * (1.0 - pr));
float inb = step(0.0, q.x) * step(q.x, 150.0) * step(0.0, q.y) * step(q.y, 52.0) * pr;
float rib = 0.55 + 0.35 * step(0.5, fract((q.x - 30.0) / 21.0)) * step(30.0, q.x);
float handle = step(q.x, 26.0) * 0.55;
float rim = step(abs(abs(q.y - 26.0) - 26.0), 3.5) * 0.35 + step(abs(q.x - 148.0), 3.5) * 0.2;
v += inb * (0.42 * rib + handle + rim + 0.06 * sin(ph * 2.0 + q.x * 0.1 + fi));
// lit once the record reaches it
float arr = i == 0 ? 3.1 : (i == 3 ? 4.05 : (i == 8 ? 5.0 : 99.0));
float lit = sm(arr, arr + 0.15, s) * (1.0 - ease(sm(5.6, 6.1, s)));
v += inb * lit * 0.45;
// the build strip: one square a slab, filled once it has landed
float tk = step(abs(p.x - 52.0 - 24.0 * fi), 5.0) * step(372.0, p.y) * step(p.y, 391.0);
v += tk * (0.14 + 0.85 * pr * step(0.97, pr));
}
// a scan that crosses the slots all the time, so the empty frame still moves
float sw = fract(t / T);
v += 0.3 * sin(3.1416 * sw) * exp(-pow(p.x - (18.0 + 684.0 * sw), 2.0) / 500.0) * step(abs(p.y - 232.0), 150.0) * step(0.5, fract((p.y - 118.0) / 74.0) < 0.7 ? 1.0 : 0.0);
// the record: leaves the first block at 3.2 s, reaches the second at 4.05 s, the workflow block at 5.0 s
float ru = clamp((s - 3.2) / 0.85, 0.0, 1.0);
float rv = clamp((s - 4.15) / 0.85, 0.0, 1.0);
vec2 pa = vec2(125.0, 144.0);
vec2 pb = vec2(360.0, 144.0);
vec2 pc = vec2(595.0, 292.0);
vec2 pos = rv > 0.0 ? mix(pb, pc, ease(rv)) : mix(pa, pb, ease(ru));
float fade = 1.0 - ease(sm(5.0, 5.3, s));
float on = step(3.2, s) * fade;
v += on * (blob(p, pos, 9.0, 1.4) + lk(p, pa, mix(pa, pb, ease(ru)), 0.6) + (rv > 0.0 ? lk(p, pb, pos, 0.6) : 0.0));
float g = s - 5.0;
if (g > 0.0 && g < 0.45) v += 0.6 * (1.0 - g / 0.45) * exp(-pow(length((p - pc) * vec2(1.0, 1.6)) - 40.0 - 70.0 * g, 2.0) / 30.0);
return v;
}]]>
1
Begin with sturdy building blocks
Compose your pipeline and internal tools from one set of parts. Data model, layout, and logic.
2
Keep going without friction
Customize freely with the coding agents you already use. Reshape the system to match how your team sells and grows.
3
Stay in charge with open source
No one else's ecosystem to be locked into. Brindle feels like ordinary software: local setup, real data, live tests, and no proprietary tooling.
In practice
What teams changed once the frame was already built
Production grade quality
A freight company treated Brindle as the finished frame for objects, roles, sign-in and automation, instead of rebuilding each piece from scratch.
Head of Platform, freight company
Agents for quick iteration
An advisory firm pointed coding agents at their schema and folded a quarter of renewal-tracking work into a week, with one engineer reviewing.
Managing Partner, advisory firm
Control without drag
A clinic group moved to a self-hosted Brindle with no vendor lock, no forced upgrade path, and system costs down by more than 80%.
Systems Lead, clinic group
Skip the rough edges that come with custom
Give your sales team a system they will enjoy
Table view
A table of six rows with name, stage and value bars; a filter fades the rows that do not match and the three that remain slide up together.
702.0) return 0.0;
float ph = TAU * t / T;
float v = 0.0;
float e = ease(sm(2.0, 2.7, t)) * (1.0 - ease(sm(4.6, 5.3, t)));
float gone = ease(sm(1.4, 1.9, t)) * (1.0 - ease(sm(5.3, 5.8, t)));
// header: three thin bars over the columns
v += rowb(p, 84.0, 60.0, 90.0, 0.2) + rowb(p, 84.0, 290.0, 60.0, 0.2) + rowb(p, 84.0, 450.0, 80.0, 0.2);
v += step(abs(p.y - 100.0), 1.6) * step(50.0, p.x) * step(p.x, 660.0) * 0.3;
for (int i = 0; i < 6; i++) {
float fi = float(i);
float keep = mod(fi, 2.0) < 0.5 ? 1.0 : 0.0;
float slot = keep > 0.5 ? fi * 0.5 : fi;
float y = 130.0 + 54.0 * mix(fi, slot, e * keep);
float vis = keep > 0.5 ? 1.0 : 1.0 - gone;
float h = hash(vec2(fi, 3.0));
float pulse = 0.06 * sin(ph + fi * 1.3);
v += vis * (rowb(p, y, 60.0, 100.0 + 70.0 * h, 0.5 + pulse) + rowb(p, y, 290.0, 40.0 + 30.0 * h, 0.9) + rowb(p, y, 450.0, 60.0 + 150.0 * (1.0 - h), 0.38 + pulse));
v += vis * step(abs(p.y - y - 26.0), 1.2) * step(50.0, p.x) * step(p.x, 660.0) * 0.14;
}
return v;
}]]>
Familiar, tidy tables
Brindle keeps it plain. Rows, columns and filters behave the way you expect, and narrow to what matters in a click.
Live sync
Two panes of rows side by side; one row is edited on the left, a packet crosses to the right pane, and the matching row there grows once it arrives.
702.0) return 0.0;
float ph = TAU * t / T;
float v = 0.0;
float back = 1.0 - ease(sm(4.4, 5.2, t));
float gl = ease(sm(0.7, 1.4, t)) * back;
float gr = ease(sm(2.3, 2.9, t)) * back;
for (int i = 0; i < 4; i++) {
float fi = float(i);
float y = 120.0 + 72.0 * fi;
float h = hash(vec2(fi, 8.0));
float ll = 90.0 + 80.0 * h + (fi == 1.0 ? 130.0 * gl : 0.0);
float lr = 90.0 + 80.0 * h + (fi == 1.0 ? 130.0 * gr : 0.0);
v += rowb(p, y, 36.0, ll, 0.5 + 0.05 * sin(ph + fi)) + rowb(p, y + 24.0, 36.0, 80.0 + 60.0 * h, 0.22);
v += rowb(p, y, 372.0, lr, 0.5 + 0.05 * sin(ph + fi + 1.0)) + rowb(p, y + 24.0, 372.0, 80.0 + 60.0 * h, 0.22);
}
// the two panes' edges
v += step(abs(p.x - 352.0), 1.6) * step(abs(p.y - 232.0), 150.0) * 0.18;
// the cursor riding the end of the edited bar while it grows
float cx = 36.0 + 90.0 + 80.0 * hash(vec2(1.0, 8.0)) + 130.0 * gl;
v += step(0.7, t) * step(t, 1.8) * blob(p, vec2(cx + 8.0, 192.0), 8.0, 1.1 * (1.0 - ease(sm(1.4, 1.8, t))));
// the packet: leaves the edited row once the edit is made, lands on the right row
float u = clamp((t - 1.6) / 0.7, 0.0, 1.0);
vec2 A = vec2(cx + 10.0, 192.0);
vec2 B = vec2(366.0, 192.0);
float on = step(1.6, t) * (1.0 - ease(sm(2.3, 2.6, t)));
v += on * (blob(p, mix(A, B, ease(u)), 8.5, 1.35) + step(abs(p.y - 192.0), 3.0) * step(A.x, p.x) * step(p.x, mix(A.x, B.x, ease(u))) * 0.45);
float g = t - 2.3;
if (g > 0.0 && g < 0.45) v += 0.6 * (1.0 - g / 0.45) * exp(-pow(length((p - vec2(480.0, 192.0)) * vec2(0.5, 1.5)) - 20.0 - 50.0 * g, 2.0) / 30.0);
return v;
}]]>
Live data, shared
An edit made by one person shows up for everyone at once, with no refresh and no merge to resolve.
Command bar
A query field with typed text over a list of rows; the list narrows to two, the top one is picked, and a packet carries it to an action pane that answers.
702.0) return 0.0;
float ph = TAU * t / T;
float v = 0.0;
float clr = 1.0 - ease(sm(4.8, 5.6, t));
// the query field: an outline, and the typed text growing in three steps
v += step(abs(abs(p.y - 86.0) - 24.0), 1.6) * step(28.0, p.x) * step(p.x, 460.0) * 0.28;
float typed = 24.0 * (step(0.5, t) + step(0.9, t) + step(1.3, t)) * clr;
v += rowb(p, 86.0, 44.0, 18.0 + typed, 0.55);
float blink = 0.5 + 0.5 * cos(TAU * 2.0 * t);
v += step(abs(p.y - 86.0), 11.0) * step(abs(p.x - (68.0 + typed)), 2.0) * (0.3 + 0.5 * blink);
// the list: five rows; rows 1, 2 and 4 do not match and fade; the others close up
float e = ease(sm(2.0, 2.5, t)) * (1.0 - ease(sm(4.8, 5.4, t)));
float gone = ease(sm(1.5, 2.0, t)) * (1.0 - ease(sm(5.4, 5.8, t)));
float pick = sm(2.6, 2.75, t) * (1.0 - ease(sm(4.8, 5.4, t)));
for (int i = 0; i < 5; i++) {
float fi = float(i);
float keep = (i == 0 || i == 3) ? 1.0 : 0.0;
float slot = i == 3 ? 1.0 : fi;
float y = 150.0 + 52.0 * mix(fi, slot, e * keep);
float vis = keep > 0.5 ? 1.0 : 1.0 - gone;
float h = hash(vec2(fi, 5.0));
float lit = (i == 0) ? pick : 0.0;
v += vis * (rowb(p, y, 44.0, 210.0 + 120.0 * h, 0.4 + 0.5 * lit + 0.05 * sin(ph + fi)) + rowb(p, y, 28.0, 10.0, 0.5));
v += vis * rowb(p, y, 392.0, 20.0 + 28.0 * h, 0.22);
}
// the action pane: a block that answers once the packet lands
float ans = ease(sm(3.65, 4.1, t)) * clr;
v += step(abs(abs(p.x - 590.0) - 106.0), 1.6) * step(abs(p.y - 232.0), 120.0) * 0.22;
v += step(abs(p.x - 590.0), 106.0) * step(abs(p.y - 232.0), 120.0) * (0.1 + 0.35 * ans) * (0.7 + 0.3 * noise(p * 0.06 + ph));
v += blob(p, vec2(590.0, 232.0), 34.0, 0.9 * ans);
// the packet: from the lit row to the pane
float u = clamp((t - 3.0) / 0.7, 0.0, 1.0);
float on = step(3.0, t) * (1.0 - ease(sm(3.7, 4.0, t)));
vec2 A = vec2(400.0, 150.0);
vec2 B = vec2(500.0, 232.0);
v += on * blob(p, mix(A, B, ease(u)), 8.5, 1.35);
float g = t - 3.7;
if (g > 0.0 && g < 0.5) v += 0.6 * (1.0 - g / 0.5) * exp(-pow(length(p - vec2(590.0, 232.0)) - 50.0 - 55.0 * g, 2.0) / 28.0);
return v;
}]]>
Straight to the action
Type a few letters, pick a match, and the action runs. Shortcuts and layouts keep daily work short.
In production
Revenue teams drive company-wide change with Brindle
Freight company
Ship a product on Brindle
The team built a driver-onboarding portal at scale, with Brindle as the operational backbone.
“We stopped asking for a feature and started shipping it ourselves. The first week we changed the data model three times and nothing broke.”
Tobias Verhagen Head of Platform
Read the case
Clinic group
Own your system end to end
The group replaced a retired vendor system with a self-hosted Brindle and cut running costs by more than 80%.
“The flexibility is what made the difference. Our needs shift week to week. When I spot a new one, two clicks later it is handled. That is a real advantage when you are moving fast.”
Maren Oduya Co-founder
Read the case
Advisory firm
Grow on a flexible foundation
The firm runs a modular Brindle setup across renewals, referrals and reporting for four practice areas.
“Self-hosting meant our records stayed ours. The agents do the tedious mapping, and one of us reviews it over coffee.”
Ines Calloway Managing Partner
Read the case
Any questions?
Things teams ask before they switch
Is Brindle really open-source? Yes. The core is published under a permissive licence, you can read every line, and you can run it on your own servers.
How long does it take to get started? Most teams have a first schema and real records in under an hour with the local setup, and a shared workspace by the end of the day.
Can I migrate from my current CRM? Yes. Import maps your existing objects and fields, and an agent can draft the mapping for you to review before anything is written.
Do I need a developer to customize Brindle? Not for everyday changes: objects, fields and views are point and click. A developer helps when you want custom logic.
Can developers extend Brindle with code? Yes. Blocks are plain TypeScript, tested and deployed like the rest of your software.
Does Brindle work with the coding agents I use? It ships a schema and command reference that agents read directly, so they can propose changes you review as ordinary diffs.
What does Brindle cost? Self-hosting is free. Hosted workspaces are priced per seat, with a flat rate for read-only users.
Stop fighting custom. Start building with Brindle.