Skip to main content
Model stylesheets let you assign LLM models, providers, and settings to workflow nodes using a CSS-like syntax. Instead of hardcoding a model on every node, you write a set of rules that target nodes by ID, class, shape, or a universal wildcard — and Fabro applies them by specificity.

Defining a stylesheet

Stylesheets are set in the model_stylesheet graph attribute:
example.fabro
digraph Example {
    graph [
        goal="Build and review a utility function",
        model_stylesheet="
            *        { model: claude-haiku-4-5;}
            .coding  { model: claude-sonnet-4-5; reasoning_effort: high; }
            #review  { model: gemini-3.1-pro-preview;}
        "
    ]

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    spec      [label="Write Spec"]
    implement [label="Implement", class="coding"]
    test      [label="Write Tests", class="coding"]
    review    [label="Code Review"]

    start -> spec -> implement -> test -> review -> exit
}
In this example:
  • spec gets Haiku (matches *)
  • implement and test get Sonnet with high reasoning (match .coding)
  • review gets Gemini Pro (matches #review)

Selectors

Each rule starts with a selector that determines which nodes it applies to:
SelectorSyntaxMatchesSpecificity
Universal*All nodes0
Shapebox, tab, hexagon, etc.Nodes with that Graphviz shape1
Class.classnameNodes with class="classname"2
ID#nodeidThe node with that specific ID3

Assigning classes

Set the class attribute on a node to target it with class selectors. Multiple classes are space-separated:
implement [label="Implement", class="coding critical"]
This node matches both .coding and .critical rules.

Properties

Stylesheets support four properties:
PropertyDescriptionExample
modelModel ID or aliasclaude-sonnet-4-5, opus, gemini-pro
providerProvider name (optional — auto-inferred from the model catalog when omitted)anthropic, openai, gemini
reasoning_effortReasoning effort levellow, medium, high
backendAgent execution backend — api (default) runs Fabro’s own tool loop, cli delegates to an external CLI tool. See Backends.cli, api
See Models for the full list of model IDs and aliases.

Specificity and cascading

When multiple rules match the same node, the rule with the highest specificity wins. This follows the same principle as CSS:
* (0) < shape (1) < .class (2) < #id (3)
For example:
*       { model: claude-haiku-4-5; }
.coding { model: claude-sonnet-4-5; }
#review { model: gpt-5.2; }
A node with id="review" and class="coding" gets gpt-5.2 because #id (specificity 3) beats .class (specificity 2). If two rules have the same specificity, the last one in the stylesheet wins.

Explicit attributes override stylesheets

A model set directly on a node attribute always takes precedence over stylesheets, regardless of specificity:
implement [label="Implement", class="coding", model="claude-opus-4-6"]
Even if .coding sets model: claude-sonnet-4-5, this node uses Opus because the explicit attribute wins.

Syntax reference

The stylesheet syntax is a simplified subset of CSS:
selector { property: value; property: value; }
  • Selectors: *, shape, .class, #id
  • Properties and values are separated by :
  • Declarations are separated by ;
  • Whitespace is flexible — newlines and indentation are ignored
  • CSS comments (/* ... */) are not supported

Full example

*            { model: claude-haiku-4-5;reasoning_effort: low; }
box          { reasoning_effort: high; }
tab          { reasoning_effort: low; }
.coding      { model: claude-sonnet-4-5;reasoning_effort: high; }
.review      { model: gemini-3.1-pro-preview;}
#final_check { model: claude-opus-4-6;reasoning_effort: high; }
This stylesheet:
  • Defaults everything to Haiku with low reasoning
  • Overrides all agent nodes (box shape) to high reasoning
  • Keeps prompt nodes (tab shape) at low reasoning
  • Routes .coding nodes to Sonnet
  • Routes .review nodes to Gemini for independent critique
  • Routes the final_check node to Opus for maximum quality