Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Write consistent, actionable validation error messages in gh-aw.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 221% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 161% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 394% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 116% | 0% |
Use this format for gh-aw validation errors. Keep messages clear, actionable, and example-driven.
[what's wrong]. [what's expected]. [example of correct usage]Make each error message answer three questions:
Avoid standalone negative wording. Pair it with expected behavior and a concrete fix.
| Avoid only-negative wording | Prefer constructive wording | |---|---| | invalid | expected + valid format/options | | cannot | requires + precondition | | must | should + example | | failed | action context + recovery step |
❌ invalid repo format: %s ✅ invalid repo format '%s' — expected 'owner/repo' format (for example: 'github/gh-aw')
❌ not in a git repository ✅ not in a git repository — run 'git init' or 'cd' to a git repository
NewValidationError vs fmt.ErrorfNewValidationError(field, value, reason, suggestion) in *_validation.go logic.field for the exact config pathreason for what failedsuggestion for an actionable fix with an examplefmt.Errorf for operational/wrapping errors (%w) where you are propagating a lower-level failure with context.fmt.Errorf("failed to X: %w", err) unless you add recovery guidance.Every suggestion should:
Example:
textUse one supported engine. ✓ Example: engine: copilot ✗ Avoid: engine: unknown
These examples follow the template and provide actionable guidance:
goreturn nil, fmt.Errorf("invalid time delta format: +%s. Expected format like +25h, +3d, +1w, +1mo, +1d12h30m", deltaStr)
✅ Why it's good:
goreturn "", fmt.Errorf("manual-approval value must be a string, got %T. Example: manual-approval: \"production\"", val)
✅ Why it's good:
goreturn fmt.Errorf("invalid engine: %s. Valid engines are: copilot, claude, codex, custom. Example: engine: copilot", engineID)
✅ Why it's good:
goreturn fmt.Errorf("tool '%s' mcp configuration must specify either 'command' or 'container'. Example:\ntools:\n %s:\n command: \"npx @my/tool\"", toolName, toolName)
✅ Why it's good:
These examples lack clarity or actionable guidance:
goreturn fmt.Errorf("invalid format")
❌ Problems:
goreturn fmt.Errorf("manual-approval value must be a string")
❌ Problems:
goreturn fmt.Errorf("invalid engine: %s", engineID)
❌ Problems:
Always include examples for:
go fmt.Errorf("invalid date format. Expected: YYYY-MM-DD HH:MM:SS. Example: 2024-01-15 14:30:00")
go fmt.Errorf("invalid permission level: %s. Valid levels: read, write, none. Example: permissions:\n contents: read", level)
go fmt.Errorf("timeout-minutes must be an integer, got %T. Example: timeout-minutes: 10", value)
go fmt.Errorf("invalid MCP server config. Example:\nmcp-servers:\n my-server:\n command: \"node\"\n args: [\"server.js\"]")
Examples can be omitted when:
go return fmt.Errorf("failed to parse configuration: %w", err)
go return fmt.Errorf("duplicate unit '%s' in time delta: +%s", unit, deltaStr)
go return fmt.Errorf("unsupported feature. See https://docs.example.com/features")
%s - strings%d - integers %T - type of value%v - general value%w - wrapped errorsFor YAML configuration examples spanning multiple lines:
gofmt.Errorf("invalid config. Example:\ntools:\n github:\n mode: \"remote\"")
Use proper YAML syntax in examples:
go// Good - shows quotes when needed fmt.Errorf("Example: name: \"my-workflow\"") // Good - shows no quotes for simple values fmt.Errorf("Example: timeout-minutes: 10")
Use the same field names as in YAML:
go// Good - matches YAML field name fmt.Errorf("timeout-minutes must be positive") // Bad - uses different name fmt.Errorf("timeout must be positive")
All improved error messages should have corresponding tests:
gofunc TestErrorMessageQuality(t *testing.T) { err := validateSomething(invalidInput) require.Error(t, err) // Error should explain what's wrong assert.Contains(t, err.Error(), "invalid") // Error should include expected format or values assert.Contains(t, err.Error(), "Expected") // Error should include example assert.Contains(t, err.Error(), "Example:") }
When improving existing error messages:
go// Time deltas fmt.Errorf("invalid time delta format: +%s. Expected format like +25h, +3d, +1w, +1mo, +1d12h30m", input) // Dates fmt.Errorf("invalid date format: %s. Expected: YYYY-MM-DD or relative like -1w. Example: 2024-01-15 or -7d", input) // URLs fmt.Errorf("invalid URL format: %s. Expected: https:// URL. Example: https://api.example.com", input)
go// Boolean expected fmt.Errorf("read-only must be a boolean, got %T. Example: read-only: true", value) // String expected fmt.Errorf("workflow name must be a string, got %T. Example: name: \"my-workflow\"", value) // Object expected fmt.Errorf("permissions must be an object, got %T. Example: permissions:\n contents: read", value)
go// Engine selection fmt.Errorf("invalid engine: %s. Valid engines: copilot, claude, codex, custom. Example: engine: copilot", id) // Permission levels fmt.Errorf("invalid permission level: %s. Valid levels: read, write, none. Example: contents: read", level) // Tool modes fmt.Errorf("invalid mode: %s. Valid modes: local, remote. Example: mode: \"remote\"", mode)
go// Missing required field fmt.Errorf("tool '%s' missing required 'command' field. Example:\ntools:\n %s:\n command: \"node server.js\"", name, name) // Mutually exclusive fields fmt.Errorf("cannot specify both 'command' and 'container'. Choose one. Example: command: \"node server.js\"") // Invalid combination fmt.Errorf("http MCP servers cannot use 'container' field. Example:\ntools:\n my-http:\n type: http\n url: \"https://api.example.com\"")
pkg/workflow/time_delta.gopkg/workflow/*_test.goWhen writing error messages, consider:
Other measured skills in the registry, with their headline benchmark lift.