An approval card is not just a payload. It is a message that a human must understand, with a requester, an approver, a decision state, an amount or scope, and a record of what was known when the decision was made. The same is true of queue items: a pending approval sits in someone's queue with an SLA and an escalation path, and its fields drive routing, reminders and audit reporting. Architecture guidance for human-in-the-loop approval workflows makes the same point about the decision record: approval gates exist to give the organization traceability for who approved what and why, with the evidence that justified the decision attached the StackAI approval workflow guide.
Once you accept that, the schema stops being an implementation detail and becomes a contract with three groups of readers:
- Old cards already stored. A card created under version 1 sits in the database, in a queue index, in an archive, or in a message topic. It will be read by code running the current schema.
- Old code still running. During a rollout, two application versions can run against the same data. A rename or a type change deployed on one side can break the other ByteByteGo on schema evolution.
- Downstream consumers. Reporting jobs, notification workers, dashboards and replay tools read the same records and often lag behind the producer by several versions.
The practical question for an approval system is narrower than general schema design: if I change a rule on this card, which stored object stops working, and how would I know before merge? ByteByteGo's account of schema evolution is blunt about the failure mode: migrations look clean in staging, then unrelated components fail in production because data written under one version is read under another ByteByteGo.
A useful contract to write down, and then encode in tests, has four clauses:
| Clause | Rule | Typical breaking edit | Test that catches it |
|---|---|---|---|
| Required | A field added as required only after every producer writes it | Adding costCenter to required in one release | Old fixture fails current schema validation |
| Optional | Fields may be added as optional with a safe default | Making a formerly required field optional without a default | Round-trip test loses no value |
| Types | Types may widen, never narrow or change meaning | amount from integer cents to float dollars | Negative test asserts the old fixture is rejected |
| Enums | New values may be added; existing values may not be removed or renamed | Dropping escalated from status enum while cards carry it | Old fixture with that status fails validation |
That table is the whole contract in miniature. Notice what is absent: nothing about field order, nothing about JSON formatting, nothing about which database column stores the field. Compatibility is about meaning and validation, and the fixtures should test exactly that.



