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,
}
}
try:
parsed = json.loads(raw)
except json.JSONDecodeError as e:
raise library.ErrRepairable(
prompt=f"The text below should be valid ticket JSON, but parsing failed:\n {e}\n\n{ticket_schema_spec}\n\nInput:\n{raw}\n\nOutput corrected JSON only — the entire object, not a patch. No code fences.",
cause=e
)
try {
data = JSON.parse(raw);
} catch (e) {
throw new Error(`The text below should be a valid ticket JSON, but parsing failed:\n ${e}\n\n${TICKET_SCHEMA_SPEC}\n\nInput:\n${raw}\n\nOutput corrected JSON only — the entire object, not a patch. No code fences.`);
}
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
}
# In Python, decoding and encoding are handled by codec functions passed to repair
def unmarshal_repair(response: str) -> dict:
cleaned = strip_code_fences(response)
return json.loads(cleaned)
// In TypeScript, codec encodes and decodes the types to strings
codec: {
encode: (v) => JSON.stringify(v),
decode: (v) => JSON.parse(v)
}
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",
},
)
library.register_with_repair(
"ParseTicketRepair",
lambda: ParseTicketOp(),
library.RepairConfig(
input_field="raw",
max_attempts=3,
prompt_prefix="You are a strict JSON corrector. Output the corrected JSON only.\n\n",
)
)
const parsed = wf.ai.repair(
rawInput,
{
name: "repair_parse",
codec: { encode: (v) => v, decode: (v) => v },
run: (raw: string) => parseTicket(raw)
}
);
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
export CLAUDE_API_KEY=<your key>
# malformed JSON
python examples/with_repair/main.py --input @examples/with_repair/testdata/dirty-format.json
# urgent ticket missing escalation_contact
python examples/with_repair/main.py --input @examples/with_repair/testdata/dirty-business-rule.json --reasoning
export CLAUDE_API_KEY=<your key>
# malformed JSON
npx tsx examples/with_repair/index.ts --input @examples/with_repair/testdata/dirty-format.json
# urgent ticket missing escalation_contact
npx tsx examples/with_repair/index.ts --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
# Speak MCP over stdin/stdout instead of running once
python examples/with_repair/main.py --mcp
# Speak MCP over stdin/stdout instead of running once
npx tsx examples/with_repair/index.ts --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"]
}
}
}