Skip to content

v0.8.0 — Array and object inputs for YAML tools

Choose a tag to compare

@callobuzz callobuzz released this 31 Aug 15:28
· 4 commits to main since this release

Added

YAML tools can declare array and object inputs.

convertInputType knew four words — string, number, boolean, enum — and its default branch mapped everything else to z.string(). A tool that needed a list therefore could not be written as YAML at all.

This was never a limit of the tool system. A built-in declares zod directly and could always take any shape; ToolEngine.execute validates with z.object(tool.input) and does not care what those schemas are. It was a limit of the authoring format — and an invisible one, which is what made it expensive:

What you wrote What happened
type: array fell through to z.string(), and the call failed at runtime with "Expected string, received array"
field left undeclared, $var used in the GraphQL z.object() stripped the undeclared key, so the argument silently never reached Shopify

The second is the dangerous shape. The motivating case is a partial fulfillment: fulfillmentCreate needs fulfillmentOrderLineItems: [{ id, quantity }] to choose which lines go in a parcel, and with that argument stripped the mutation means fulfil everything on this fulfillment order. A shipper who selected two of five lines would have shipped all five and been told it worked.

Fixed arity was checked as a workaround before adding this, and does not work: building the list from $id_1, $qty_1, $id_2, $qty_2 is rejected at GraphQL document validationNullability mismatch on variable $id_2 and argument id (ID / ID!) — whether or not the second slot is supplied, because a nullable variable cannot be used where a non-null argument is expected.

The new types

input:
  line_items:
    type: array
    required: false
    min: 1                   # an empty list is refused, not read as "all of them"
    items:
      type: object
      properties:
        id:
          type: string
          required: true
        quantity:
          type: number
          required: true
  • type: array requires items:, the declaration every element is validated against. min / max bound the element count, not a value.
  • type: object requires properties:.
  • Both throw at load time when that declaration is missing, rather than loading a tool that cannot work.
  • Array elements are always required, whatever items: says. A GraphQL list is [X!], so an optional element schema would let [null] through as a valid entry.

Two traps, now documented

  • An omitted optional list is not an empty one. GraphQL drops an argument whose variable was never provided, so leaving a list out means "the API decides" while [] means "none". Use min: 1 when empty must be refused rather than interpreted.
  • Undeclared properties are stripped, silently. A key not listed under properties: never reaches Shopify.

Notes

Backwards compatible — array and object were previously unreachable words, so no existing tool changes behaviour.

The default fallthrough to z.string() is deliberately unchanged. It is the mechanism behind both symptoms above and throwing would be more honest, but it would stop a server booting on a typo that currently half-works — a separate decision from adding the two missing types, and not one for a minor release.

Verified

Against a live Shopify store, not only in unit tests: a YAML create_fulfillment declaring line_items as an array of { id, quantity } objects had a two-line selection accepted by Shopify's schema — rejected on business grounds, as intended, because the fulfillment order id was deliberately one that cannot exist. Nothing was fulfilled and no customer was emailed. An empty list was refused locally by min: 1 before reaching Shopify.

9 new loader tests (32 in that file, 673 in the suite). Typecheck clean, biome clean, build clean.

Full changelog: v0.7.1...v0.8.0