Visual DNA & @ References
How Visual DNA binding works in prompts, and how to use @ references to control which asset plays which role in a generation.
Visual DNA is how Kolbo keeps a character, style, product, or scene looking
the same across many generations. The agent creates a DNA profile from a few
reference images, gives it a name, and then references it by @<name> in
future prompts.
This page explains how the binding actually works on the kolbo-api side, so the agent (and you) can use it precisely.
How @<name> is resolved
When you submit a prompt that contains @<name> together with a
visual_dna_ids array, the backend:
- Parses every
@<token>out of the prompt. - Looks up the matching Visual DNA by its
namefield (case-insensitive) inside your library (your personal DNAs plus any shared ones). - Replaces the
@<token>in the prompt with the DNA's storedsystemPrompt— the full character/style description that was baked in when the DNA was created. - Sends the rewritten prompt + the DNA's reference images to the generation model.
That last step is the important one: @<name> is the mechanism that
injects the DNA's description into the prompt. If the prompt has no
@<name>, the DNA's systemPrompt is never injected — the visual_dna_ids
slot becomes a near-no-op, and the model has to guess what you wanted the
DNA to control.
The cardinal rule
Whenever you pass a
visual_dna_idsarray — even just one id — the prompt MUST reference each DNA by@<name>.
Wrong:
prompt: "אסתר wearing a gold necklace, half-body portrait"
visual_dna_ids: ["vdna_abc"] // a DNA whose name is "esther_model"The engine sees plain Hebrew text "אסתר" and has no idea it should bind to
vdna_abc. The DNA's systemPrompt is never injected.
Right:
prompt: "@esther_model wearing a gold necklace, half-body portrait"
visual_dna_ids: ["vdna_abc"] // esther_modelThe engine swaps @esther_model for the DNA's full systemPrompt and the
character stays consistent across generations.
Naming Visual DNAs
name is matched case-insensitively against the literal @<token> in
the prompt.
Strong recommendation: pick a single-token, lowercase, no-space name.
esther_model, dana, tokyo_neon, brand_red — these are the names
that travel cleanly across multilingual prompts and never need defensive
punctuation. Reason: the parser stops the @<token> match at the first
space (also at .,!? punctuation, see Termination rules),
so a name with spaces forces every prompt that mentions it to either add
explicit punctuation or eat the rest of the sentence as part of the
lookup. The fallback truncates progressively, but the failure mode is
silent — a mis-bound name simply drops, and the DNA never injects.
Other rules:
- Any-language characters are allowed (
name: "אסתר"→ write@אסתר). Whatever string you stored is the string you must type. - Spaces are technically permitted but discouraged — use
_between words (old_town, notOld Town). The Kolbo Code agent (and thecreate_visual_dnaMCP tool's description) collapses user-provided multi-word names to underscores automatically for this reason. - Names cannot equal
Image1,Video2, etc. — those are reserved (see below). - Max 100 characters.
Termination rules
The parser keeps consuming words after @ until it hits any of:
- punctuation
.,,,!, or? - two or more spaces in a row
- another
@ - end of the prompt
So @Sarah Johnson, wearing red matches the DNA named Sarah Johnson. A
prompt like @Maya holds a hammer would try to look up Maya holds a hammer, fail, and then fall back to progressively shorter prefixes
(Maya holds a, Maya holds, Maya) until one matches.
That fallback is best-effort — write punctuation or a double-space between
the @name and the next word whenever you can:
@maya, wearing a leather jacket, walks through @tokyo_street.@Image1 / @Video1 / @Audio1 — positional reference tags
A separate namespace, used for plain reference assets that you pass in
reference_images, reference_videos, reference_audio, or
source_images / source_videos. These are positional:
@Image1=reference_images[0]@Image2=reference_images[1]@Video1=reference_videos[0]@Audio1=reference_audio[0]- …and so on.
The parser excludes @Image\d+, @Video\d+, @Audio\d+ from Visual DNA
lookup, so they never collide with a DNA name. Validation on Visual DNA
creation also rejects names matching Image\d+.
Mixing Visual DNAs and positional references
The two namespaces compose freely in the same prompt:
prompt: "@maya, wearing the leather jacket from @Image1,
walks through the coffee shop in @Image2"
visual_dna_ids: ["vdna_8f2c"] // maya
reference_images: ["jacket.jpg", // @Image1
"coffee_shop.jpg"] // @Image2Here @maya injects the DNA's character description; @Image1 /
@Image2 tell the engine which uploaded image is the wardrobe vs. the
location.
For video/audio references the same pattern applies — name the asset's role explicitly:
prompt: "Lipsync @Video1's speaker to the dialogue track @Audio1,
keep the room tone of @Video1; ignore @Audio2 (music bed)."
reference_videos: ["takes/take-03.mp4"] // @Video1
reference_audio: ["dialogue.wav", // @Audio1
"music_bed.mp3"] // @Audio2{USER_PROMPT} placeholder
A DNA's systemPrompt may contain the literal placeholder {USER_PROMPT}.
When the backend replaces @<name> with the systemPrompt, it substitutes
{USER_PROMPT} with the user intent — i.e. the original prompt with all
@mentions stripped. This lets a DNA template say:
<character description> ... {USER_PROMPT} ... <consistent style notes>
…and the user's actual instruction ("walking through the rain at golden hour") slots into the middle. You normally don't need to think about this; it's worth knowing only because it explains why prompts feel like they "come out longer" when a DNA is bound.
Unresolved mentions
If a @<name> doesn't match any DNA in your library (and the truncation
fallback also fails), the mention is silently removed from the prompt.
This protects against typos producing garbage prompts, but it means a
mistyped name is a silent failure mode — the DNA simply doesn't apply.
Verify before generating: call list_visual_dnas, confirm the DNA exists,
copy the exact name into the prompt.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| DNA passed but character looks generic | Prompt referenced the character in plain text, never wrote @<name> | Replace the plain-text mention with @<name> |
@Maya resolves to the wrong character | Two DNAs share a name; lookup is ambiguous | Rename one DNA via create_visual_dna with a more specific name |
@<name> followed by text gets over-captured | No punctuation or double-space between @<name> and the next word | Add a comma, period, or two spaces — @maya, walks… |
| Two reference images blend together | Prompt didn't name them with @Image1 / @Image2 | Tag each asset's role explicitly |
| Visual DNA "doesn't apply" silently | Mention was unresolved (typo or DNA not in library) and got stripped | list_visual_dnas first; copy the exact name |
When to use Visual DNA
- Same character across multiple images or a campaign →
generate_image/generate_creative_directorwithvisual_dna_ids. - Animating a character with elements models (Seedance 2, Kling O3
Reference, Grok Imagine, Veo 3.1, …) →
generate_elementswithvisual_dna_ids. Note:generate_videodoes not apply DNA — usegenerate_elementsfor character-consistent video. - Consistent brand style across a campaign →
generate_creative_directorwithvisual_dna_ids+ a moodboard. - Place a character in a specific environment → create one
characterDNA and oneenvironmentDNA; reference both with@<name>syntax.
Reference
The parser lives in
kolbo-api/src/utils/visualDnaMentionParser.js
— see parseVisualDnaMentions, resolveVisualDnaMentions, and
replaceVisualDnaMentions for the exact behavior.