A support ticket pipeline that ingests raw JSON and runs it through two repair-wrapped stages. First, a JSON parser corrects malformed input or schema violations. Second, a routing validator enforces business rules and round-trips the offending ticket through XML when it needs the LLM to fix it. Both wire-format paths of the wrapper — string repair and struct repair — exercised in one workflow.
What this example teaches
*library.ErrRepairable from a deterministic op to opt in to LLM-driven recoveryUnmarshalRepair — once with simple string handling, once round-tripping through XMLlibrary.RegisterWithRepair and different RepairConfig valueslibrary.WithReasoningLog
From the graph builder's perspective the two repair-wrapped vertices look exactly like the
inner ops — same input / output field names. The wrapper only activates when the inner
Run returns *library.ErrRepairable.
*ErrRepairable from the inner opParseTicketOp attempts JSON deserialisation
and then runs schema checks (id format, email shape, valid priority, non-empty summary).
Both failure paths return *library.ErrRepairable carrying a self-contained
prompt that includes the bad input and the schema spec — everything the LLM needs
to produce a corrected JSON object without further context.
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
return &library.ErrRepairable{
Prompt: fmt.Sprintf(
"The text below should be valid ticket JSON, but parsing failed:\n %s\n\n%s\n\nInput:\n%s\n\nOutput corrected JSON only — the entire object, not a patch. No code fences.",
err, ticketSchemaSpec, raw,
),
Cause: err,
}
}
UnmarshalRepair on the input typeThe wrapper hands the LLM's response to
UnmarshalRepair. For the parse stage the input is a thin TicketRaw
wrapper around a string — just strip code fences and assign. For the validate stage the
input is *TicketInput and the wrapper expects XML, so
UnmarshalRepair calls xml.Unmarshal.
func (t *TicketRaw) UnmarshalRepair(response string) error {
t.Text = stripCodeFences(response)
return nil
}
func (t *TicketInput) UnmarshalRepair(response string) error {
cleaned := stripCodeFences(response)
if err := xml.Unmarshal([]byte(cleaned), t); err != nil {
return fmt.Errorf("xml.Unmarshal: %w", err)
}
return nil
}
One init() call per wrapped op. Pick the
input field to repair, set an attempt budget, and supply a prompt prefix that primes the
model for the response shape it should emit. Different stages can have different budgets
(3 attempts to fix a JSON parse, 2 to fix a routing violation).
library.RegisterWithRepair(
"ParseTicketRepair",
func() *ParseTicketOp { return &ParseTicketOp{} },
library.RepairConfig{
InputField: "Raw",
MaxAttempts: 3,
PromptPrefix: "You are a strict JSON corrector. Output the corrected JSON only.\n\n",
},
)
library.RegisterWithRepair(
"ValidateRoutingRepair",
func() *ValidateRoutingOp { return &ValidateRoutingOp{} },
library.RepairConfig{
InputField: "Ticket",
MaxAttempts: 2,
PromptPrefix: "You are a strict XML ticket corrector. Output corrected XML only.\n\n",
},
)
Sample inputs ship under testdata/. Pass
--reasoning to enable WithReasoningLog and see exactly which
attempt corrected the ticket.
export CLAUDE_API_KEY=<your key>
# malformed JSON
go run ./examples/with-repair --input @examples/with-repair/testdata/dirty-format.json
# urgent ticket missing escalation_contact
go run ./examples/with-repair --input @examples/with-repair/testdata/dirty-business-rule.json --reasoning
The same binary is dual-mode. Pass --mcp and instead of running once and exiting it speaks the Model Context Protocol over stdin/stdout, exposing the entire workflow as a single MCP tool, parse_and_validate_ticket. The Go SDK derives the tool's input schema from the workflow's UserInput struct and validates every tools/call request against it; the AI repair attempts happen server-side, so the caller only ever sees the validated ticket or a clean error.
# Speak MCP over stdin/stdout instead of running once
go run ./examples/with-repair --mcp
Register it with any MCP client by pointing the client at that command:
{
"mcpServers": {
"with-repair": {
"command": "go",
"args": ["run", "./examples/with-repair", "--mcp"]
}
}
}