An agent card, mocked. The orb is the real generator, seeded from the agent name and running its own states.

Pendo Orb

Company Pendo

Year 2026

Type

If a product is going to have many AI agents, each one needs to be recognisable and each one needs to show what it is doing. The obvious answer is to draw them, which means a designer draws another one every time someone adds an agent.

I made them generate instead. Hash the agent's id, get a stable identity out the other end. Nova always looks like Nova, and nobody drew Nova.

Rather run it than read about it? The generator is in section 01.

Sky, Bunny, Miami and Sun, the original four, being generated right now by the same render core that was handed to the Agent Analytics team. Not a video of the component. The component. Put your cursor on one.

None of that started in code. The whole look was worked out in Figma first, and the best of those renders then went into the build as the reference the generator had to match. Section 04 is that half of it.

67,949

distinct on-brand orbs available from the seed space, none of them drawn by hand

That number is the concept's. The product's is different, and the gap is the interesting part. Agent Analytics generates from live Refresh tokens, and FY27 renamed a ramp underneath them: the olive green the generator had been drawing is lime now, and green is a new, bluer hue. Same rules, same seeds, different answers. 64,050 valid palettes instead of 67,949, and across 2,000 seeds the product agrees with the concept 52.5% of the time.

52.5%

of seeds still produce the palette the concept produced, after one token rename upstream

The rules travelled. The exact colours did not. Computed identity guarantees that a seed always maps to a coherent palette, which is not the same promise as a seed always mapping to the same palette, and I had been describing it as though it were.

Drift did worse than recolour things. The mauve ramp carries two rungs, levels 30 and 40, that the table this generator shipped with never had, and miami mode draws its fourth colour from exactly that pair. The draw found nothing, handed the renderer an undefined colour, and threw. That is 8.3% of every seed, failing from the first day and never once in front of me, because the seeds I happened to look at were not in the 8.3%. Seed 9 was the first one that was.

Filling in the two real values fixes all of them. It also moves the rendered colour for 23.7% of seeds, because a level with more candidates to choose from changes what every draw after it picks. A missing colour was never only a missing colour.

Designer and engineer, mid July 2026. A framework-agnostic render core with a thin Vue wrapper over it, running on live design tokens, shipping no image assets at all.


The split that makes it work. Which agent you are talking to is carried by colour, derived from the seed. What that agent is currently doing is carried by motion and a halo. Idle, thinking, responding, degraded, error, offline.

Keeping those on separate channels means status never costs identity. A degraded Nova still reads as Nova.

Everything is driven by live tokens, so a theme change carries through without anyone re-exporting anything.

Agent Orbs generator
The generator, live: type a seed, drag the sliders, watch the identity hold · open full screen

An orb is four coloured blobs, blurred until their edges dissolve and composited over each other. Wherever two overlap, their colours average. Average the wrong two and you get mud: that flat dishwater grey-brown that makes a generated gradient look like a mistake.

Mud has three causes. Colours that were never saturated to begin with. Everything sitting at the same middling lightness with nothing dark to anchor it. And vivid hues on opposite sides of the wheel, which cancel toward grey as they mix.

So the generator constrains its inputs instead of inspecting its output. It draws four tokens, tests them against six rules, and throws the draw away on any failure, up to 500 times.

29

most rerolls any seed needed, measured across 300,000 seeds, against the 500 the generator is allowed

Four of the rules are lightness windows, one per slot, and they are the boring ones that work.

  • base0.120.50
  • b0.350.70
  • a0.681.00
  • c0.701.00
Lightness windows, to scale. Plus the rule the axis cannot show: c has to sit at least 0.32 above base, so there is always something dark holding the gradient down.

The fifth rule is the one doing the real work, and the one worth arguing about. Any two tokens that are both saturated and sit 150 degrees or more apart in hue kill the draw.

I went back and measured the rule instead of trusting it, and it is blunter than the section above makes it sound.

56.9%

of the palettes that clear all four lightness windows are then killed by the hue rule alone

That is most of the legal space, thrown away by one line. Some of it deserves to go. The problem is what else goes with it.

The rule compares hues. It does not know where on the canvas those two colours actually land. Two of the four blobs, a and b, sit at opposite corners and barely touch, so a rejection that cites only that pair is describing a mix the renderer never performs. That is 6.4% of all rejections, every one of them a palette thrown away for a reason that is not true of the image.

Here is the rule being right and being wrong, at the same angle, drawn by the same core:

The rule is rightrejected on base against b

base blue/90 against b yellow/80 · 162°
base teal/80 against b coral/60 · 166°

The rule is wrongrejected on a against b

a teal/20 against b pink/50 · 173° · on an indigo/70 base
a indigo/40 against b yellow/90 · 177° · on a pink/70 base

The top row is what the rule is for. The base is the wash under the entire orb, so when the base is half the offending pair those two hues really do average across the whole surface, and you get the olive and the sludge green. The bottom row fails the same rule at a wider angle and comes out as some of the best orbs in the set.

If you go looking for the offending colours in that bottom row you will not find them, and that is the finding rather than a fault in the picture. Those two tokens are a and b, a pair of small blobs in opposite corners, so what you actually see is the base. A rule that rejected these was reasoning about a mixture the renderer never puts on screen.

One more thing I only found by reading the code back. The rule is written as a window, rejecting anything from 150 to 210 degrees, which reads like it is catching a band around exact opposition. But the hue distance function folds anything past 180 back down, so it can never return more than 180 and the upper bound has never once fired. It is a threshold wearing a window's clothes. Harmless, and not what I thought I had written.

I would not remove the guard. A cheap filter that errs toward rejecting is the right shape for this problem, because the cost of a rejection is one more reroll out of an allowed 500 and the cost of a miss is an agent whose identity looks like a mistake. But "no orb looks muddy" was the claim, and the honest version is that no orb looks muddy because the filter throws away far more than the muddy ones.

The surface came first, and it came out of Figma. Dither against grain against bayer, plus glass and crystal treatments I dropped for being heavy and looking chopped at small sizes. Weeks of it, before a line of the render core existed.

The reference render. This is the file the build was pointed at

That one is the pivot of the whole project. When it came time to build the thing in code, I did not write a description of what an orb should look like and hope. I handed over the render itself, as the target, and worked against it until the canvas matched.

That is a different job from prompting. A written brief for this would have been guesswork twice over, once by me describing a gradient in words and once by the model interpreting it. An artifact does not need interpreting. It is the spec, it is exact, and every argument about whether the output is right becomes a comparison rather than an opinion.

The rest of the studies are what that reference was chosen out of. The dot grid is doing almost all the work here, and the corner radius is under the microscope at the same time.

Dither at full strength, warm ramp
The same study on a cool ramp

Then a genuinely annoying discovery: the same orb rendered differently on different monitors, and the difference was display calibration rather than anything in the code. At the sizes these actually appear, texture is close to the noise floor, and how much of it survives depends on hardware I do not control.

That reframed the decision. The right amount of texture is not the amount that looks best on my screen, it is the amount that still reads as intentional on the worst screen it will land on. I took it down.

So the reference above is not what shipped. The four orbs at the top of this page are, at grain 0.19, and scrolling between the two is the size of the cut.

The light and dark schemes did not pass AA when I checked them properly, so there is a whole pass late in the history fixing that, along with corrections to four claims in my own documentation that turned out to be overstated.

I mention it because the AA pass came after the pretty part, which is the wrong order and the usual order.

What the product needed was narrow. Generate a unique orb per agent so people can tell them apart and pick the one they want. That is the whole brief, and the generator above satisfies it.

The six states are already more than that. The fleet view is a lot more.

20 healthy · 1 degrading · 1 error · 2 offline

  • Atlasresponding
  • Novaidle
  • Onyxthinking
  • Vegaresponding
  • Cobraidle
  • Kilothinking
  • Orbitresponding
  • Zephyrdegraded
  • Fluxidle
  • Emberresponding
  • Halothinking
  • Ridgeoffline
  • Deltaidle
  • Solresponding
  • Krakenthinking
  • Prismidle
  • Vertexresponding
  • Echoerror
  • Titanidle
  • Lynxthinking
  • Fableresponding
  • Quartzoffline
  • Driftidle
  • Sagethinking

Twenty four agents, each seeded from its own name, all running the same core. Trigger the incident and eight of them go wrong in three different ways. Nova drops to error, and it is still recognisably Nova, which is the entire argument for putting identity and status on separate channels rather than turning a troubled agent red.

I built this knowing it could not ship. It turned into a study of what an agent might look like if it reacted to analytics signals, which is a question the product has not asked yet. I went there because I liked the product and wanted to see how far the idea held.

There was a further version that did not survive. Instead of six discrete states, four continuous inputs, activity driving pulse, load driving rotation, health driving warmth, latency driving drag. It is more expressive than the discrete set and it is more honest about a system that is never wholly in one state.

It died on something simple. Nothing upstream emits those numbers. Activity, load, health and latency are not values the product can hand a component today, so a design driven by continuous signals had nothing to be driven by, and the honest response was to stop designing it. Six named states are what an application can actually pass in, so six named states is what the engine carries.

This is past concept-stage now. It was packaged as a real handoff, a render core, a Vue wrapper and a README, and the engineer working on agent analytics merged it: APP-163275 landed on 31 July behind a per-subscription feature flag, replacing the sparkle icon at 28px on agent cards and 16px in report rows. It also went to the person working on the brand site.

I am not going to dress that up as more than it is. Nothing in the product currently passes the orb a status, so although the engine carries all six states, both mount points leave it at the default and every orb renders the same resting breath. The identity shipped. The behaviour did not.

The section above is the reason I state that as a wiring problem rather than an open design question. The behaviour exists, it runs, and you can put a fleet of it under load in a browser. What is missing is a product that has anything to say to it.

The reason I like it is the mechanic. Identity that is computed rather than authored scales to any number of agents for free, and it fails in a nice direction: an agent nobody planned for still gets a coherent, on-brand, unique appearance the moment it exists.


all work