Skip to main content
← All docs
FFlowcore
    flowcore

    Define and evolve event contracts

    Bergur Davidsen·Updated 2026-07-15

    |Open in||History

    Flowcore resources identify where events belong. Your application code defines what each event payload means.

    A reliable contract has three parts:

    1. A stable flow type and event type name
    2. A runtime schema that validates payloads
    3. Documentation describing semantics, ownership, compatibility, and sensitive fields

    TypeScript types improve developer experience, but they disappear at runtime. Use a runtime schema so invalid payloads are rejected before they become durable history or corrupt a projection.

    Keep platform declarations and payload schemas separate

    Flowcore v2 resource manifests declare platform resources and behavior. They contain names, descriptions, parent relationships, access controls, delete protection, and optional sensitive-data configuration.

    Application schemas define payload fields and validation rules.

    For example, a resource manifest can declare that order.placed.0 exists:

    apiVersion: data-core.flowcore.io/v1
    kind: FlowType
    metadata:
      name: order.0
      tenant: acme-development
    spec:
      dataCore: commerce
      description: Order lifecycle events
      eventTypes:
        - name: order.placed.0
          description: An order was accepted for processing

    The payload contract belongs in application code:

    import { z } from "zod"
     
    export const orderPlacedSchema = z.object({
      orderId: z.string().uuid(),
      customerId: z.string().uuid(),
      currency: z.string().length(3),
      lines: z.array(
        z.object({
          productId: z.string(),
          quantity: z.number().int().positive(),
          unitPriceMinor: z.number().int().nonnegative(),
        }),
      ).min(1),
      placedAt: z.string().datetime(),
    })
     
    export type OrderPlaced = z.infer<typeof orderPlacedSchema>

    The manifest is the platform resource registry. The Zod schema is the runtime payload contract.

    Changing a schema does not automatically change the Flowcore resource declaration. Adding a new event type name does require creating or declaring that platform resource.

    Register contracts with Pathways

    Register the schema so Pathways can validate writes and handler input:

    const app = pathways.register({
      flowType: "order.0",
      eventType: "order.placed.0",
      schema: orderPlacedSchema,
      writable: true,
      flowTypeDescription: "Order lifecycle events",
      description: "An order was accepted for processing",
    })

    Pathways uses literal flow and event names to infer valid paths. The schema provides the input and output payload type.

    Descriptions also control declarative provisioning ownership:

    • A description means Pathways may create or update the resource when provisioning is enabled.
    • No description means the resource is expected to exist already.

    Decide whether code or manifests own each production resource. Do not let both workflows modify the same definitions independently without a clear policy.

    Validate at both boundaries

    Validate when publishing and when consuming.

    Producer validation prevents known-invalid payloads from entering history. Consumer validation protects projections from events produced by older, external, or incorrectly configured publishers.

    Do not assume that successful ingestion proves every domain invariant. In multi-producer environments, all authorized producers must follow the contract.

    A consumer should handle validation failure explicitly:

    • Include the event ID and contract path in diagnostics.
    • Avoid logging secrets or the complete sensitive payload.
    • Do not advance the checkpoint as if processing succeeded.
    • Route the failure into an operator-visible retry or exception process.

    Design payloads for long-lived use

    An event contract often outlives the service version that first produced it.

    Use stable identifiers

    Prefer domain identifiers that remain meaningful across services and projections. Avoid relying on local database row positions or ephemeral request IDs as the only identity.

    Make units explicit

    A field named amount is ambiguous. Prefer names or documented contracts that state whether the value is major currency units, minor units, decimal text, or another representation.

    Represent time intentionally

    Distinguish event occurrence, business validity, and storage time. If a domain timestamp belongs in the payload, name it clearly, such as placedAt or effectiveFrom.

    Avoid internal snapshots by accident

    Do not serialize an ORM object and call it an event contract. Database models include storage concerns, nullable migration fields, and private implementation details that may not belong in durable history.

    Keep secrets out

    Never publish passwords, API keys, bearer tokens, private signing keys, or raw payment credentials. If the event needs a secure external object, publish a controlled reference rather than the secret itself.

    Share contracts without coupling implementations

    When several TypeScript services publish or consume the same events, place schemas and inferred types in a versioned contract package.

    A useful package can export:

    • Flow and event path constants
    • Zod schemas
    • Inferred TypeScript types
    • Contract documentation or examples
    • Compatibility tests

    Keep handlers, database models, and service-specific clients outside the shared contract package. Sharing the contract should not force every consumer to adopt the producer's architecture.

    Pin contract-package versions and upgrade deliberately. A floating dependency can change validation behavior without an application code review.

    Understand compatibility

    Contract compatibility is about whether existing producers, consumers, retained events, and replay tools continue to work.

    Usually compatible with care

    • Adding an optional field
    • Expanding an enum only when consumers tolerate unknown values
    • Relaxing a validation restriction without changing meaning
    • Adding metadata that consumers ignore

    Even these changes require testing. Strict schema parsing, exhaustive switches, or generated clients can make an apparently additive change breaking.

    Usually breaking

    • Removing or renaming a field
    • Changing a field's type
    • Making an optional field required
    • Changing units or encoding
    • Narrowing allowed values
    • Moving fields into a new nested structure
    • Changing the business fact represented by the event
    • Reusing a name for a different lifecycle

    Breaking changes require a new event type version.

    Use explicit version migration

    Suppose order.placed.0 stores a numeric total, but the organization needs exact minor-unit values and explicit currency allocation. Do not silently change the old field's meaning.

    Introduce order.placed.1 with a new schema.

    A controlled migration normally follows these steps:

    1. Define and review the new contract.
    2. Create the new event type resource.
    3. Update shared contract packages.
    4. Make consumers understand the new version.
    5. Dual-publish or translate during a bounded transition when required.
    6. Compare new and old projections.
    7. Stop producing the old version.
    8. Keep old consumers or migration tooling as long as retained old history must be replayed.
    9. Mark the old event type as retired in documentation and ownership records.

    Do not rewrite retained order.placed.0 events to look like version 1. Historical events must keep their original meaning.

    Dual-publishing requires care

    Publishing both versions can ease migration, but it creates two facts for one business action unless consumers understand the relationship.

    If dual-publishing:

    • Use shared correlation and causation identifiers.
    • Document which version is authoritative during the transition.
    • Prevent consumers from applying both versions as separate business changes.
    • Define a date or release condition for stopping the old version.
    • Monitor production counts for both streams.

    An alternative is a translation pathway that consumes the old version and emits the new version. This centralizes conversion but still needs idempotency and traceability.

    Test contracts against retained examples

    Maintain representative fixtures for each contract version. Tests should cover:

    • Valid historical payloads
    • Optional and missing fields
    • Boundary values
    • Unknown enum values where relevant
    • Late-arriving or corrected data
    • Sensitive-field handling
    • Replay through current handlers

    Before tightening validation, test the new schema against retained payloads. A schema can be logically cleaner but incompatible with real history.

    Separate schema evolution from resource deletion

    Stopping publication of an old version is not the same as deleting its event type or truncating its history.

    A retired event type may still be needed for:

    • Audit
    • Replay
    • Old projection rebuilds
    • Migration verification
    • Regulatory retention
    • Incident investigation

    Keep the resource and history unless a reviewed lifecycle requirement says otherwise.

    Contract review checklist

    Before approving a new event type:

    • The event name describes a completed fact.
    • The version suffix is explicit.
    • The payload has a runtime schema.
    • Stable identifiers and units are documented.
    • Required and optional fields are intentional.
    • Domain time semantics are clear.
    • Secrets are excluded.
    • Sensitive fields are identified.
    • The owning producer and expected consumers are recorded.
    • The resource provisioning owner is clear.
    • Compatibility and retirement expectations are documented.
    • Fixtures test realistic payloads.

    Before changing an existing contract:

    • Retained payloads were tested.
    • Existing consumer behavior was reviewed.
    • Strict parsing and exhaustive switches were considered.
    • A new version is used for incompatible meaning or structure.
    • Migration, dual-publishing, or translation has an end condition.
    • Replay behavior was verified.

    Continue with Govern access, sensitive data, and resource lifecycle before putting contracts into production.

    PreviousDesign your Flowcore event modelNextGovern access, sensitive data, and resource lifecycle