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:
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 byid(if missingid, runtime generates UUID)delete- delete stub byid
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
effects:
- action: upsert
stub:
service: UserService
method: GetByID
input:
equals:
id: "{{ index (index (index .Request \"fields\") \"id\") \"string_value\" }}"
output:
data:
enabled: trueClient-stream Example
Use .Requests to read aggregated stream messages:
effects:
- action: upsert
stub:
service: AuditService
method: LastEvent
input:
equals:
id: "{{ index (index (index (index .Requests 1) \"fields\") \"event\") \"string_value\" }}"
output:
data:
ok: trueBehavior 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:
- prepare all effects (template render + validation)
- 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 stubLoginwith 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
actionis requiredupsertrequires non-emptystubdeleterequires non-emptyid
Invalid effects are rejected by REST/MCP validation.
Testing Strategy
Use minimal high-value coverage:
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
Small validation tests (REST/MCP path):
- missing
stubon upsert - missing
idon delete - unknown action
- missing
E2E with grpctestify v3.11.0
Reference scenario is available in inventory examples:
examples/projects/inventory/test_effects.yamlexamples/projects/inventory/test_effects.gctf
Run it with standard flow:
# terminal 1
go run . examples/projects/inventory -s examples/projects/inventory
# terminal 2
grpctestify examples/projects/inventoryWorkflow validated by this scenario:
- target request hits fallback stub
- create request triggers
effects.upsert - update request re-upserts same
id(deterministic update) - cleanup request triggers
effects.delete - register -> auth chained effects remove profile
- broken prepare request proves all-or-none behavior
Practical Usage Pattern
For deterministic updates, provide fixed id inside effect-generated stub.
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.