Skip to main content

Schema Harvesting

An action's parameters don't have to be written out in axag-* attributes. Generators combine three sources, highest precedence first:

SourceWhere it comes fromsource in the manifest
Declaredaxag-required-parameters / axag-optional-parameters, req= / opt= in the macro(omitted)
Schema bindingA Zod schema or OpenAPI operation named by axag-schema or the CLI's bindings configzod or openapi
HarvestedThe form's controls, constraints and labelsharvested:html

Precedence

  1. A higher source decides whether a parameter is required or optional and keeps every field it sets.
  2. Lower sources fill fields the higher one left out, such as a description from a <label> or maxLength from a schema.
  3. A parameter declared by name only ('["email"]') has no type of its own, so its type also comes from a lower source.
  4. Parameters found only in a lower source are added.
  5. A binding's x-axag-risk-level and summary are used only when the annotation declares no risk level or description.

Which controls are harvested

The parameter scope of an annotated element is:

  • the element itself, if it is a <form>;
  • its form, if it is a submit button (<button> without type, type="submit", or <input type="submit|image">), including a form linked with form="id";
  • the element named by axag-params-from="#id".

Inside that scope, <input>, <select>, <textarea> and elements with a form-control role (textbox, combobox, switch, checkbox, slider, spinbutton) are harvested, plus controls elsewhere on the page with form="<scope id>". Disabled controls and hidden, file, submit, button, reset and image inputs are skipped.

HTML to parameter mapping

MarkupParameter
axag-parameter, else name, else idname (converted to snake_case)
required, aria-required="true"listed in required_parameters
type="number" / "range", role="slider" / "spinbutton"type: "integer", or "number" when step is fractional or any
min, max (aria-valuemin, aria-valuemax)min, max
type="email" / "url" / "date" / "datetime-local"format: "email" / "url" / "date" / "datetime"
minlength, maxlength, patternminLength, maxLength, pattern
<select> optionsenum
<select multiple>type: "array", items.enum
Radio buttons sharing a nameenum of their values
Single checkbox, role="switch"type: "boolean"
Checkboxes sharing a nametype: "array", items.enum
aria-labelledby, aria-label, <label for>, wrapping <label>description
Group <legend> or role="radiogroup" labeldescription of a radio or checkbox group
aria-describedbyappended to description
axag-parameter-type, -required, -description, -format, -enum, -min, -max, -min-length, -max-length, -patternoverride the harvested value

JSX spellings (htmlFor, maxLength, minLength) read the same as HTML. JSX expressions such as required={isRequired} are dynamic and are ignored.

Schema bindings

Inline binding
<form axag="write:user.invite!medium" axag-schema="zod:./src/schemas/user.ts#InviteUser"></form>
RefMeaning
zod:<file>#<export>A Zod 4 z.object() export; #<export> defaults to default
openapi:<file>#<operationId>Path and query parameters plus the JSON request body of an operation
openapi:<operationId>Same, using the openapi document set in the CLI config

See Bind Zod or OpenAPI Schemas.

RuleChecks
AXAG-LINT-029A submitted control has no name, so agents can't fill it
AXAG-LINT-030The form disagrees with a schema-sourced parameter in the manifest (required, type or enum)
AXAG-LINT-031An annotated button, link or control has no accessible name
AXAG-LINT-032A harvested parameter has no label or description

To turn harvesting off, run axag scan … --no-harvest.