Skip to content

Effects (Stateful Stub Transitions) v3.11.0

effects allow a matched stub to mutate runtime stub storage.

Use this when you need stateful workflows like:

  • register -> login
  • warmup request -> next request changes behavior
  • one RPC enabling/disabling another RPC stub

Model

Effects live inside regular stub definition:

yaml
service: AuthService
method: Register
input:
  equals:
    email: "john@example.com"
output:
  data:
    ok: true
effects:
  - action: upsert
    stub:
      service: AuthService
      method: Login
      input:
        equals:
          email: "john@example.com"
      output:
        data:
          token: "token-john"

Supported actions:

  • upsert - create/update stub by id (if missing id, runtime generates UUID)
  • delete - delete stub by id
yaml
effects:
  - action: delete
    id: "8f2d9b80-8c66-42a6-9f3d-c331f3aee6dd"

Session Inheritance (Automatic)

Effects inherit session from parent matched stub automatically.

  • parent stub in global session (session: "") -> generated/deleted targets are global
  • parent stub in named session (session: "test-a") -> generated/deleted targets are in same session

No scope field.

Dynamic Templates in Effects

effects.upsert.stub is processed by template engine before validation/upsert.

Available context is same as dynamic output templates:

  • .Request
  • .Requests
  • .Headers
  • .MessageIndex
  • .RequestTime
  • .StubID

Unary Example

yaml
effects:
  - action: upsert
    stub:
      service: UserService
      method: GetByID
      input:
        equals:
          id: "{{ index (index (index .Request \"fields\") \"id\") \"string_value\" }}"
      output:
        data:
          enabled: true

Client-stream Example

Use .Requests to read aggregated stream messages:

yaml
effects:
  - action: upsert
    stub:
      service: AuditService
      method: LastEvent
      input:
        equals:
          id: "{{ index (index (index (index .Requests 1) \"fields\") \"event\") \"string_value\" }}"
      output:
        data:
          ok: true

Behavior by RPC Type

Effects are applied in all paths:

  • unary
  • server streaming
  • client streaming
  • bidirectional streaming

Apply moment: after successful match and template preparation for response path.

Effect execution is two-phase:

  1. prepare all effects (template render + validation)
  2. apply prepared operations

If any effect fails in prepare phase, the whole effects list is skipped for that request (all-or-none at prepare stage).

Chaining Effects (register -> login -> profile)

Yes, you can define effects inside a stub generated by another effects.upsert.

This enables multi-step workflows across requests:

  • request 1 (Register) upserts stub Login with its own effects
  • request 2 (Login) triggers nested effects (for example, deleting profile stub)
  • request 3 (GetProfile) returns fallback/not-found

Important:

  • chaining happens across subsequent requests, not recursively inside same request
  • for deterministic updates/deletes, prefer fixed UUIDs in generated stubs

Validation Rules

  • action is required
  • upsert requires non-empty stub
  • delete requires non-empty id

Invalid effects are rejected by REST/MCP validation.

Testing Strategy

Use minimal high-value coverage:

  1. One E2E workflow covering transitions end-to-end:

    • fallback -> create(upsert) -> update(upsert same id) -> cleanup(delete)
    • chained effects across subsequent requests
    • prepare failure all-or-none behavior
  2. Small validation tests (REST/MCP path):

    • missing stub on upsert
    • missing id on delete
    • unknown action

E2E with grpctestify v3.11.0

Reference scenario is available in inventory examples:

  • examples/projects/inventory/test_effects.yaml
  • examples/projects/inventory/test_effects.gctf

Run it with standard flow:

bash
# terminal 1
go run . examples/projects/inventory -s examples/projects/inventory

# terminal 2
grpctestify examples/projects/inventory

Workflow validated by this scenario:

  1. target request hits fallback stub
  2. create request triggers effects.upsert
  3. update request re-upserts same id (deterministic update)
  4. cleanup request triggers effects.delete
  5. register -> auth chained effects remove profile
  6. broken prepare request proves all-or-none behavior

Practical Usage Pattern

For deterministic updates, provide fixed id inside effect-generated stub.

yaml
effects:
  - action: upsert
    stub:
      id: "a1f7c2be-6e31-4e9f-b46d-5ec4d626fd2b"
      service: AuthService
      method: Login
      input:
        equals:
          email: "john@example.com"
      output:
        data:
          token: "token-john"

Without id, each upsert creates a new UUID stub.