GoX for LLM agents
July 3, 2026 · View on GitHub
GoX compiles .gox templates to .go. Write HTML-like templates as typed Go expressions; the gox CLI generates .x.go next to each .gox.
Prerequisites
Run gox ver first — it must print a version (e.g. v0.1.27). Without gox on PATH, generation fails and builds error with undefined: example for every template.
Install:
- Prebuilt binary (recommended): download from GitHub Releases, put on
PATH. - From source:
make install— requires Go, Cargo, and a native toolchain (bundled Rust formatter). Nogo install ...@latestshortcut — native deps prevent it.
Stop and ask before installing if the environment forbids network access or global installs.
The Go module side is automatic: go get github.com/doors-dev/gox (or just import and go mod tidy). Keep CLI and module versions in sync — generated files carry a version marker and refuse mismatched runtimes. gox ver and the // Managed by GoX vX.Y.Z header must agree.
Editor extension (always recommended). Without it, .gox looks broken, go-to-definition/completion across .gox↔.go are disabled. Extensions also start the GoX language server (proxies gopls).
- VS Code:
doors-dev/vscode-gox - Neovim:
doors-dev/nvim-gox
Default stack rule
- If
go.modhasgithub.com/doors-dev/doors— do NOT install or usegoxx. This doc does not cover Doors-specific APIs. - If GoX is used without Doors — install
github.com/doors-dev/goxxby default. It provides HTTP render, parallel rendering, and class/proxy helpers most standalone GoX apps need.
Check first: grep doors-dev/doors go.mod. If Doors is present, skip goxx.
Golden rule
Always edit .gox files. Never edit/hand-write .x.go files. Never write templates in cursor style directly. .x.go is overwritten on every gox gen (and by the language server on save). The cursor API is for runtime extension points (Editor, Proxy, Printer, custom Modify), not authoring.
Don't:
- Hand-write
gox.Elem(func(cur gox.Cursor) error { ... })as authoring style — that's what.goxcompiles to. - Create new
.x.gofiles (suffix is reserved; GoX may delete orphans). - Treat
.x.goas source of truth — read.goxfirst. Generated.x.gois useful only as a debugging/reference aid for lowered cursor calls, source-map positions, or low-level APIs.
Workflow
gox fmt # format .gox and .go (and embedded <script>/<style>)
gox gen # regenerate .x.go for current directory
gox gen ./pkg # regenerate a specific path
go run . # build/run as normal Go
After editing .gox, run gox gen before go build/go run/go test. "undefined: MyElem" usually means missing regen.
Always run gox gen after gox fmt. Formatting shifts positions and invalidates the source map.
A typical package has all three side by side:
main.go # regular Go
page.gox # template source (edit this)
page.x.go # generated (do not edit)
Syntax essentials
A .gox file is a Go file plus the elem keyword and HTML literals. Use .gox only when the file needs template syntax (elem, HTML literals, fragments, placeholders, raw blocks, template control flow). Otherwise use .go. Top-level Go declarations (import, type, func, methods) are always normal Go and live outside elem bodies.
Old syntax compatibility
Old forms ~func { ... } and ~{ ... } may appear in existing code. gox fmt
automatically converts them to ~({ ... }) (GoX expression) and ~~ ... ~~
(Go snippet). Keep the gox binary up to date — run gox ver to check.
HTML literals are Go expressions
Inside .gox, <tag>...</tag> is a gox.Elem value. Use anywhere a Go expression goes:
var greeting gox.Elem = <h1>Hi</h1>
type Card struct { Body gox.Elem }
card := Card{Body: <p>hello</p>}
func make() gox.Elem { return <b>x</b> }
gox.Elem implements Main() gox.Elem, satisfying gox.Comp.
elem keyword
Shorthand for a function/method returning gox.Elem:
elem Greeting(name string) {
<h1>Hello, ~(name)!</h1>
}
Equivalent generated API to:
func Greeting(name string) gox.Elem {
return <h1>Hello, ~(name)!</h1>
}
Render-time vs call-time: an elem body evaluates when the element renders. A regular function returning <...> runs Go code before the return immediately when called. Idiom: use elem with a top ~~ ... ~~ setup block:
elem Page() {
~~ /* render-time setup */ ~~
<main>...</main>
}
Visibility is standard Go: elem Foo exported, elem foo package-private.
Method form (typically gox.Comp.Main):
elem (u User) Main() { <li>~(u.Name)</li> }
Anonymous elem() { ... } exists. Prefer named helpers; use anonymous only when an inline template fn is the clearest fit (not as a generic empty-arg wrapper).
Pitfall: elem is a reserved keyword. Cannot be used as variable/parameter/field name (e.g. Proxy(cur, elem gox.Elem) is a parse error — rename to el).
Go snippet: ~~ ... ~~
elem body is template mode. Plain Go statements (x := 1, if err != nil, sort.Slice(...), etc.) cannot appear bare. Wrap them in ~~ ... ~~:
elem UserList() {
~~
type User struct { Name string }
users := []User{{Name: "Ada"}, {Name: "Ben"}}
~~
<ul>
~(for _, u := range users {
<li>~(u.Name)</li>
})
</ul>
}
Inside ~~ ... ~~ you're in the generated render function (returns error).
Top-of-elem setup block (before any HTML emitted): validation, data loading, derived values, whole-component guards. From there, return nil skips the whole element; return err aborts with error:
elem MaybePanel(show bool) {
~~ if !show { return nil } ~~
<section>Visible</section>
}
Don't return nil after output starts — it exits before tags close, leaving broken HTML:
elem BrokenPanel() {
<div>
~~ return nil ~~ // bad: exits before </div>
</div>
}
Inside open markup, use ~(if ...) or an inline ~({ return nil }) to skip only a child:
elem OptionalChild(show bool) {
<div>
~// prefereable form:
~(if show { <span>Visible</span> })
~// in case of complex logic:
~({
if one {
return nil
}
if second {
return nil
}
/* ... */
return <strong>Ready</strong>
})
</div>
}
Returning a real non-nil error mid-markup is not recommended — the whole render aborts and the outer renderer handles failure. Prefer expicit render of err != nil branch.
HTML tags create Go scopes. Variables declared inside a tag body aren't visible to siblings. Declare shared values in a top-level ~~ ... ~~:
elem SharedValue() {
~~ label := "GoX" ~~
<h1>~(label)</h1>
<p>~(label)</p>
}
Placeholders: ~(expr)
<p>~(user.Name)</p>
<p>~(a, " ", b, " ", c)</p> // multi-arg, left-to-right
Parens omittable only for literals (string, numeric, composite):
<p>~"hello" ~42 ~User{Name: "Z"}</p>
Pitfalls:
~name(bare identifier) is a parse error. Always~(name).- whitespace:
~"a" ~"b"rendersaband~"a"~"b"rendersab. For space to appear:~("a", " ", "b").
Text whitespace
Template indentation and blank lines are normilized, but spaces that are part of text content are preserved. A leading or trailing space next to real text is intentional and appears in output:
<span> Text</span> // <span> Text</span>
<span>Text </span> // <span>Text </span>
<span>Text ~(v)</span> // text node is "Text ", then v
For multi-line text, indentation used to line up the template is removed. If the line has an extra space before or after the actual text, that extra space is preserved:
<span>
\t Text
</span>
// renders: <span> Text</span>
<span>
\tText
</span>
// renders: <span>Text</span>
Adjacent text-only lines are joined with a single space (One then Two renders One Two). Blank lines and whitespace-only lines render nothing. Text next to tags does not get an automatic separator: write an explicit leading/trailing space in the text when you need one.
gox fmt removes indentation and blank/edge whitespace that has no output effect; spaces that would be emitted are preserved.
Control flow: ~(if ...), ~(for ...)
Wrap the statement in ~(...):
~(if loggedIn {
Welcome, ~(name)!
} else if guest {
Please log in.
} else {
Bye.
})
~(for _, u := range users { <li>~(u.Name)</li> })
~(for i := 0; i < 3; i++ { <span>~(i)</span> })
Fragments: <>...</>
Group children without a wrapper tag:
elem Layout(body gox.Elem) { <body>~(body)</body> }
Layout(<>
<h1>Title</h1>
<p>Paragraph</p>
</>)
Attributes
- String/numeric literal:
<div class="card" tabindex=0>. - Go expression in parens:
<div id=(id) title=(user.Bio)>. - GoX expression (eval at render):
<input checked=({ return u.Agreed })>. - Bare attribute:
<input required>≡required=(true). nilorfalse→ attribute omitted (cosmetic stray space may remain).true→ bare name:checked=(true)→checked.- Names case-sensitive:
class≠Class(both emitted). - Output order: alphabetical, not source order.
Attribute values follow the same rules as placeholders (~(expr)), but = replaces ~:
- Literal:
class="card",tabindex=0 - Expression in parens:
id=(id) - GoX expression:
checked=({ return ok })
Exception: attribute values are always single-value; multi-value =(a, " ", b) like ~(a, " ", b) is not supported.
Never use ~ in an attribute value — id=~(id) or checked=~({ ... }) is wrong. The = already signals a GoX value.
Attribute Modifiers
Most modifiers come from third-party libs (goxx.Class, component kits). Attach inside parens inside the opening tag:
<button (goxx.Class("primary"))>Go</button>
<button (goxx.Class("a"), TestID("save"))>Multi</button> // comma-separated
Writing your own Modify is fine for reusable attribute bundles (design-system presets, analytics, form conventions):
type Modify interface {
Modify(ctx context.Context, tag string, attrs gox.Attrs) error
}
type PrimaryCTA struct { Label string }
func (p PrimaryCTA) Modify(_ context.Context, _ string, attrs gox.Attrs) error {
attrs.Get("class").Set("btn btn-primary")
attrs.Get("role").Set("button")
attrs.Get("aria-label").Set(p.Label)
return nil
}
// Usage: <button (PrimaryCTA{Label: "Save"})>Save</button>
Mutate via attrs.Get(name).Set(value) — not attrs.Set(...) (doesn't exist). For inline use: gox.ModifyFunc(func(ctx, tag, attrs) error { ... }).
Void / self-closing elements
Standard HTML void tags (<br>, <hr>, <img>, <input>, <meta>, <link>, …) accept <br>, <br/>, <br /> — all render <br>. </br> is an error.
Reading third-party docs: naming → syntax
AttrMod/ "modifier" → modifier syntax:<tag (x)>.Proxy→ proxy syntax:~>(x) nextItem.- Both (e.g.
goxx.Class) → default to modifier. Use proxy only when you can't reach the target tag (wrapping a component whose outer tag you don't author):~>(goxx.Class("test").Remove("test2")) ~(test2())
Picking the wrong syntax usually produces a compile error or a no-op, not a silent bug.
Per-attribute value hooks: Mutate and Output
Run on individual attribute values (not the whole attribute set):
type Mutate interface {
Mutate(name string, prev any) (newValue any) // combine with previous value under same name
}
type Output interface {
Output(w io.Writer) error // value renders into attribute slot; GoX still escapes
}
Mutate builds class-style accumulators. Output controls serialization while keeping default escape.
Text escaping
Text/placeholders are HTML-escaped:
<p>~("<script>")</p> // → <script>
Raw block <:>...</:> emits literal HTML, whitespace preserved verbatim:
<svg viewBox="0 0 24 24">
<:><path d="..." /></:>
</svg>
Useful for static SVG/HTML fragments. Never pipe untrusted input through it.
Attribute values are entity-escaped only. GoX does not filter URL schemes: a user-controlled value in href/src can carry javascript: or data: URLs (unlike html/template). This is intentional — validate untrusted URLs in application code, or implement scheme filtering in a custom Printer where you can respect the places that legitimately need such URLs.
Components (gox.Comp)
Anything with Main() gox.Elem is a component. In .gox, implement with elem method syntax:
type Card struct { Title string }
elem (c Card) Main() {
<article>~(c.Title)</article>
}
Render via normal placeholder (no JSX-style <MyComp/>):
~(myComponent) // identifier needs parens
~User{Name: "Z"} // composite literal, parens optional
elem helper vs component struct
Plain elem Helper(args...) for small stateless fragments with few params:
elem Badge(label string) { <span class="badge">~(label)</span> }
Use a component struct when:
- Many positional args would be needed
- Named fields read clearer (
Title,Body,Items, callbacks, state) - Multiple render helpers share data (receiver methods)
- Repeated composite-literal usage
- Constructor needs setup/defaults
- Must satisfy
gox.Comp
Keep HTML shape intact when extracting: render parent wrappers in the parent, call helpers/components inside. Move a wrapper into a component only when the component owns it.
<section>
~Card{Title: "Build", Body: <p>Use GoX</p>}
~Card{Title: "Review", Body: <p>Check output</p>}
</section>
Patterns:
Data-shaped with child slot:
type Card struct {
Title string
Body gox.Elem
}
elem (c Card) Main() {
<article>
<h2>~(c.Title)</h2>
~(c.Body)
</article>
}
Receiver helpers sharing fields:
type Menu struct {
Active string
Items []MenuItem
}
elem (m Menu) Main() {
<ul>~(for _, item := range m.Items { ~(m.item(item)) })</ul>
}
elem (m Menu) item(item MenuItem) {
<li class=({
if item.Slug == m.Active { return "active" }
return nil
})>
<a href=(item.Path)>~(item.Title)</a>
</li>
}
Constructor returning gox.Comp to hide setup:
func NewSearch(users []User) gox.Comp {
return searchBox{Users: users}
}
type searchBox struct { Users []User }
// elem (s searchBox) Main() { ... }
Page composition with named slots:
type PageShell struct { Header, Body, Footer gox.Elem }
elem (p PageShell) Main() {
<div class="page">
~(p.Header)
<main>~(p.Body)</main>
~(p.Footer)
</div>
}
Don't use Main as a field name — collides with the render method (or tempts ~(p.Main) rendering the method value). Use Body, Content, Children, MainContent.
For "wrap children in a tag" → elem Layout(body gox.Comp). Named config/state/multiple methods → component.
Children / slot pattern
Pass Elem/Comp as arg or struct field. Fragments build multi-node children:
elem Card(title string, body gox.Comp) {
<article>
<h2>~(title)</h2>
~(body)
</article>
}
~(Card("Hi", <>
<p>first</p>
<p>second</p>
</>))
Same idea via struct field of type gox.Elem or gox.Comp (the latter accepts plain gox.Elem too).
Comments
~// single-line template comment (not emitted)
~/* multi-line template comment */
<!-- emitted HTML comment -->
GoX expression: ~({ ... })
Render-time evaluation; the return value (text, component, or HTML literal) is inserted at that point. Use when logic exceeds a simple ~(if ...):
<div>
~({
user, err := db.Get(id)
if err != nil { return <span>error</span> }
switch user.Role {
case "admin":
return <strong>~(user.Name)</strong>
}
return Card(user)
})
</div>
For simple reuse → named elem. For simple conditions → ~(if ...). Works in attributes too.
Proxies: ~>(p) nextItem
A Proxy captures the next renderable item at render time (element, component placeholder, ~({...}), raw block, text, control-flow, or placeholder).
Captures one item only. ~>(p) Text ~(dd) captures only Text. Group siblings into one item: wrapper element, fragment, or multi-value placeholder ~>(p) ~("Text ", dd).
Chain: ~>(p1) ~>(p2) item ≡ ~>(p1, p2) item. Outermost first; p2 wraps the original, then p1 wraps the result.
Most proxies come from third-party libs (goxx.Parallel, goxx.Class as proxy, goxx.ProxyMod). You import and attach them.
Don't write a custom Proxy unless you truly need a low-level rendering transform. Last resort. To wrap content → component/slot. To set attributes → Modify. To attach attributes through a wrapping component → goxx.ProxyMod or goxx.Class. Custom proxies require careful cursor lifecycle.
Custom Proxy is for transforming captured output before emission — rewriting attrs on descendants, filtering printers, render metrics, retargeted output.
Common usage:
~>(goxx.Parallel()) <section>~(SlowStats())</section>
~>(Track) ~({ return <span>computed</span> })
~>(Track) ~("Text ", dd)
~>(Track) Text
~>(p1, p2) <div>chained</div>
Type and sample (rarely needed):
type Proxy interface {
Proxy(cur gox.Cursor, e gox.Elem) error // param name can't be `elem` in .gox
}
func (wrap) Proxy(cur gox.Cursor, e gox.Elem) error {
if err := cur.Init("section"); err != nil { return err }
if err := cur.Submit(); err != nil { return err }
if err := e(cur); err != nil { return err }
return cur.Close()
}
For one-offs: gox.ProxyFunc(func(cur, e) error { ... }).
Rendering at runtime
elem := Greeting("World")
elem.Render(ctx, w) // writes HTML to io.Writer
elem.Print(ctx, customPrint) // streams jobs to a custom Printer
gox.Elem is templ-compatible (implements Render(ctx, w) error).
Cursor.Any value handling
~(expr) calls Cursor.Any (or Many for multi-arg). Dedicated handling:
string,[]stringgox.Elem,[]gox.Elemgox.Comp,[]gox.Compgox.Job,[]gox.Jobgox.Editorgox.Templ(anything withRender(ctx, w) error, e.g.templ.Component)[]any
Else: escaped fmt.Fprint. nil Elem/Comp render nothing.
Raw HTML
<:>...</:> for static raw. For programmatic raw output, drop an Editor through a placeholder:
~(gox.EditorFunc(func(cur gox.Cursor) error {
return cur.Raw("<mark>unescaped</mark>")
}))
Never pipe untrusted input through these.
Runtime extension interfaces
Reach for these only when ordinary templating can't express the need. They live in .go files, not .gox.
gox.Editor—Edit(cur gox.Cursor) error. Direct cursor for low-level emission. Usegox.EditorFuncfor one-offs.gox.Proxy—Proxy(cur gox.Cursor, e gox.Elem) error. Intercepts next renderable. Prefer existing proxies.gox.Modify—Modify(ctx, tag, attrs Attrs) error. Head-level attribute transformer (<tag (Modifier)>). Usegox.ModifyFunc.gox.Mutate—Mutate(name, prev any) any. Value-level: combine with existing attribute under same name.gox.Output—Output(w io.Writer) error. Value controls own serialization (still escaped).gox.Printer— consumes theJobstream. Custom printers can buffer/transform/reroute.
Cursor lifecycle when writing Editor / low-level code:
- Regular:
Init(tag)→AttrSet/AttrMod→Submit()→ children →Close(). - Void:
InitVoid(tag)→AttrSet→Submit()(noClose). - Container:
InitContainer()→ children →Close()(no tag).
Before Submit() you may mutate attributes; after, the head is frozen but children may be emitted.
goxx — the standard extension package
Reminder: only when the project doesn't use Doors. If go.mod has github.com/doors-dev/doors, stop.
go get github.com/doors-dev/goxx
HTTP render helper
goxx.Render buffers the full response before any byte is written, so render failure can still send a clean error status:
func handlePage(w http.ResponseWriter, r *http.Request) {
out, err := goxx.Render(r.Context(), Page())
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
if err != nil {
http.Error(w, "render failed", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
out.WriteTo(w)
}
To serve a custom GoX error page on failure: render it after checking ctx errors. If the error-page render also fails, fall back to http.Error — safe, no bytes written yet.
For non-HTTP code passing a printer to elem.Print(ctx, printer), use goxx.NewPrinter(w) and goxx.WriterError(err) to distinguish writer vs render failures.
Parallel rendering
Mark independent slow fragments with ~>(goxx.Parallel()). They render on background workers; output order stays deterministic.
elem Page() {
<main>
<h1>Dashboard</h1>
~>(goxx.Parallel()) <section>~(SlowStats())</section>
~>(goxx.Parallel()) <aside>~(SlowSidebar())</aside>
</main>
}
Use for DB queries, outbound HTTP, FS reads, heavy compute. Default pool: 7 workers + caller; tune via goxx.NewPrinter(w, goxx.WithWorkers(n)) (0 = unbounded).
Custom printer: goxx.WithPrinter(factory) (+ goxx.WithFlat() if printer wants expanded content rather than *gox.JobComp). Rarely needed.
goxx.Class — three-in-one class helper
Builds an immutable class set. Inputs split with strings.Fields (variadic and space-separated equivalent).
goxx.Class("button", "primary")
goxx.Class("button primary")
goxx.Class("button").Add("primary").Filter("hidden")
Three attachment styles:
// 1. Modifier (merges with class="..." on same element)
<button (goxx.Class("button primary")) class="wide">Save</button>
// → class="wide button primary"
// 2. Attribute value (not recommended)
<button class=(goxx.Class("button", "primary"))>Save</button>
// 3. Proxy — propagates through containers/components to first real element
~>(goxx.Class("button primary")) <button>Save</button>
Filter removes matches regardless of order:
goxx.Class("button").Filter("hidden").Add("hidden").String() // "button"
Useful for stripping a class baked into a component:
~>(goxx.Class("primary").Filter("disabled")) ~(BaseButton())
goxx.ProxyMod — attach Modify through wrappers
Carries a modifier through leading components/containers to the first real element, applies once. Good for cross-cutting attributes (test ids):
func TestID(id string) gox.Proxy {
return goxx.ProxyMod(gox.ModifyFunc(func(_ context.Context, _ string, attrs gox.Attrs) error {
attrs.Get("data-testid").Set(id)
return nil
}))
}
elem Toolbar() {
~>(TestID("save-button")) ~(SaveButton())
}
Captured item must begin with element/component/container that resolves to an element. Text first → error. (goxx.Class as proxy uses this machinery.)
Common pitfalls
- Edited
.x.goby hand — gone on nextgen. Edit.gox. - Wrote new templates in cursor style (
gox.Elem(func(cur)...)) in.go— write.goxinstead. - Plain Go statements bare in
elembody — wrap in~~ ... ~~. - Render-time setup before
return <...>in regular func — runs at call time, not render. Useelem+ top~~ ... ~~. - Forgot
gox genafter edit — "undefined" build errors. - Forgot
gox genaftergox fmt— source map drifts. - Used
elemas identifier name — reserved keyword. Rename. return nilinside open markup — breaks HTML. Guard at top ofelem, or use~(if ...)/~({ return nil })for child-only skips.- Variable from tag scope referenced later — tags create scopes. Declare in top-level snippet.
~namewithout parens — parse error. Always~(name). Parenless = literals only.- Mixed
classandClass— separate attributes. Pick one casing - lower by default. - Relied on attribute source order — output is alphabetical.
- Tried
<MyComponent/>— no JSX. Use~(myComponent)or composite-literal placeholder. - Used
Mainas field name — collides with render method. Rename. - Pure-Go file as
.gox— use.gowhen no GoX/HTML syntax. - Expected different
elemvisibility — same as Go (uppercase = exported). ~(...)in attribute — text/template positions only. Useid=(id),checked=({ ... }).- Called
attrs.Set(...)inModify— method isattrs.Get(name).Set(value). </br>/</input>— void tags have no closing. Use<br>,<br/>,<br />.- Unescaped via
~(untrustedHTML)— that escapes.<:>...</:>orgox.EditorFunc + cur.Rawfor trusted only. - Whitespace between placeholders —
~(a) ~(b),~(a)~(b)have no space,~(a, " ", b)has. - Expected raw template indentation/newlines — normalized. Intentional spaces preserved. Use raw blocks for verbatim.
- Expected proxy to capture multiple siblings — captures one. Group via fragment/wrapper/multi-value placeholder.
- Custom
Proxyfor ordinary wrapping/attributes — use components/Modify/goxx.ProxyModinstead. - Imported
goxbut unused in.gox— fine; generated.x.goreferencesgox.Elemforelemand HTML syntax, so module must be ingo.mod. - Version drift — generated files have version markers; CI must use matching
gox. - Added
goxxto a Doors project — skipgoxx. - Raw
http.HandlercallingElem.Render(ctx, w)directly — failure leaves bytes written. Prefergoxx.Render(buffers first).