gRPC Services skill for Codex
Protos that evolve, calls that always have deadlines. Codex designs .proto files with versioned packages, reserved field numbers, field masks and page tokens, then runs buf lint and buf breaking before generating code. Handlers return the correct status code with rich error details, every client call carries a deadline, retries are limited to idempotent methods, and long-lived HTTP/2 connections are balanced with L7 proxies or connection ageing.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- grpc-services Installs to ~/.agents/skills/grpc-services/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install grpc-services The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project grpc-services Installs to .agents/skills/grpc-services/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/grpc-services.zipunzip -o grpc-services.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) grpc-services PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $grpc-services, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and implements gRPC services with Protocol Buffers - package and message layout, field numbering and evolution, buf lint and breaking checks, status codes and rich error details, deadlines and cancellation, retries, streaming, load balancing and health checks. Use when writing or reviewing .proto files, adding RPCs, choosing between unary and streaming, mapping errors to gRPC codes, debugging DEADLINE_EXCEEDED or uneven load across pods, or setting up buf in CI.
Use it when
- Writing or changing .proto files and service definitions
- Choosing status codes, deadlines or retry policy for RPCs
- Setting up buf lint and breaking-change checks in CI
- Debugging DEADLINE_EXCEEDED or uneven load across backends
Use something else when
- The API is public HTTP/JSON for third parties: use rest-api-design
- You need durable async messaging between services: use rabbitmq-messaging
How to check the result
buf lint and buf breaking pass against main, grpcurl calls return expected payloads and status codes, and health checks respond SERVING.
The mistake it prevents
Making client calls without a deadline, so a stuck dependency holds goroutines and connections open indefinitely and cascades the outage upstream.
A well-built gRPC service has protos that evolve without breaking deployed clients, a deadline on every call, errors mapped to the right status code with machine-readable details, and load spread across backends even though HTTP/2 connections are long-lived.
Workflow
- [ ] 1. Read the proto layout and toolchain
- [ ] 2. Design the API surface in .proto
- [ ] 3. Lint and check for breaking changes
- [ ] 4. Generate code and implement handlers
- [ ] 5. Map errors, set deadlines, configure retries
- [ ] 6. Verify with grpcurl and tests
1. Read the layout
- Read
AGENTS.md, then look forbuf.yaml,buf.gen.yaml,proto/trees, orprotocinvocations in Makefiles. Determine where generated code lives and whether it is committed. - Note the package naming scheme and whether the repo follows Google AIP conventions
(
List/Get/Create/Update/Deletemethods,page_token,FieldMask).
2. Design the proto
syntax = "proto3";
package acme.orders.v1;
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
rpc UpdateOrder(UpdateOrderRequest) returns (Order);
rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);
}
message Order {
string id = 1;
OrderStatus status = 2;
int64 total_minor_units = 3;
string currency = 4;
google.protobuf.Timestamp create_time = 5;
optional string note = 6; // presence matters: unset vs empty
reserved 7; // was coupon_code, removed in v1.4
reserved "coupon_code";
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPED = 3;
}
message ListOrdersRequest {
int32 page_size = 1; // server caps, e.g. 100
string page_token = 2; // opaque
string filter = 3;
}
message ListOrdersResponse {
repeated Order orders = 1;
string next_page_token = 2; // empty when no more pages
}
message UpdateOrderRequest {
Order order = 1;
google.protobuf.FieldMask update_mask = 2;
}
Rules:
- Version the package (
acme.orders.v1). A new major version is a new package. - One request and one response message per RPC, even if they are tiny today. Never use
google.protobuf.Emptyas a request; you cannot add fields later. - Enums: zero value is
<ENUM>_UNSPECIFIED, values are prefixed with the enum name. - Field numbers 1-15 take one byte on the wire; give them to frequent fields.
- Use
optional(proto3 presence) when “not set” must differ from the zero value. - Updates take a
FieldMaskso clients cannot accidentally blank fields they did not send. - Money as integer minor units plus currency (or
google.type.Money), neverdouble.
3. Evolution rules
| Change | Safe on the wire? |
|---|---|
| Add a field with a new number | Yes |
| Rename a field (same number and type) | Wire-safe, breaks JSON and generated code |
| Remove a field | Only if you reserved its number and name |
| Change a field’s type or number | No |
| Reuse an old field number | Never; it corrupts data from old clients |
| Add an enum value | Yes; old clients see it as unknown/zero |
Move fields into a new oneof | No |
Run buf on every change:
# buf.yaml
version: v2
modules:
- path: proto
lint:
use:
- STANDARD
breaking:
use:
- FILE
buf lint
buf breaking --against '.git#branch=main'
buf generate
Use WIRE_JSON instead of FILE for the breaking rule set if only wire and JSON
compatibility matter (no shared generated code). buf generate with remote: plugins
needs network; in an offline sandbox use local: plugins already on PATH or ask first.
4. Implement handlers
- Generated server interfaces: implement every method; embed the
Unimplemented...Serverstruct in Go so new RPCs do not break compilation. - Pass the incoming context through every downstream call (database, HTTP, other RPCs) so cancellation and deadlines propagate.
- Validate requests at the top of each handler (or with
protovalidaterules if the project uses them) and returnINVALID_ARGUMENTwith field details.
5. Errors, deadlines, retries
Status code decision table:
| Situation | Code | Client may retry? |
|---|---|---|
| Bad input, independent of system state | INVALID_ARGUMENT | No |
| Entity missing | NOT_FOUND | No |
| Create of something that exists | ALREADY_EXISTS | No |
| System not in required state (e.g. order already shipped) | FAILED_PRECONDITION | No, until state changes |
| Concurrency conflict, transaction aborted | ABORTED | Yes, at a higher level |
| No or bad credentials | UNAUTHENTICATED | No |
| Authenticated but not allowed | PERMISSION_DENIED | No |
| Quota or rate limit hit | RESOURCE_EXHAUSTED | Yes, with backoff |
| Transient: server shutting down, dependency down | UNAVAILABLE | Yes, with backoff |
| Deadline passed | DEADLINE_EXCEEDED | Only if idempotent |
| Bug, invariant broken | INTERNAL | No |
| Method not implemented | UNIMPLEMENTED | No |
Never return UNKNOWN deliberately, and never put stack traces in status messages.
st := status.New(codes.InvalidArgument, "invalid order update")
st, err := st.WithDetails(&errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{
{Field: "order.currency", Description: "must be an ISO 4217 code"},
},
})
if err != nil {
return nil, status.Error(codes.Internal, "building error details")
}
return nil, st.Err()
Deadlines:
- Every client call sets a deadline; there is no default and a missing one can hang
forever. Go:
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond); defer cancel(). - Size deadlines from the caller’s budget, not the callee’s p99. Downstream calls inherit the remaining budget automatically when the context is propagated.
- Servers should check
ctx.Err()before expensive work and stop when cancelled.
Retries via service config (client side), only for idempotent methods:
{
"methodConfig": [{
"name": [{ "service": "acme.orders.v1.OrderService" }],
"retryPolicy": {
"maxAttempts": 4,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE"]
}
}]
}
Streaming decisions
| Need | Shape |
|---|---|
| Request/response under a few MB | Unary |
| Server pushes updates or a large result set | Server streaming |
| Client uploads many items, one summary back | Client streaming |
| Interactive, both sides send independently | Bidirectional |
Long-lived streams need: a deadline or explicit cancellation, resume tokens in messages so clients can reconnect where they left off, and application-level flow awareness (do not buffer unbounded messages in memory).
Load balancing and operations
- HTTP/2 multiplexes all calls over one connection, so an L4 load balancer pins a client
to one backend. Use an L7 proxy (Envoy, a service mesh) or client-side balancing with
round_robinagainst a headless Kubernetes service. - Set server
MaxConnectionAge(for example 5 minutes, plus a grace period) so clients periodically reconnect and pick up new pods. - Expose
grpc.health.v1.Healthand use it for readiness probes (Kubernetes supports nativegrpcprobes). - Default max receive message size is 4 MB in most implementations; stream or chunk instead of raising it casually.
- Browser clients need gRPC-Web or Connect; plain gRPC does not run in browsers.
Gotchas
- Widening
int32toint64is wire-compatible, but old clients silently truncate values outside the 32-bit range and generated code changes; treat it as breaking. - Proto3 scalars without
optionalcannot distinguish 0/"" from unset; PATCH-style updates silently zero fields unless a field mask is used. - Retrying non-idempotent RPCs on
DEADLINE_EXCEEDEDdouble-applies writes. - Enabling server reflection in production exposes the full API surface; gate it.
- Keepalive pings more frequent than the server permits trigger
GOAWAYwithtoo_many_pings; align client keepalive with server enforcement policy.
Verification
buf lint && buf breaking --against '.git#branch=main'
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext -d '{"id":"ord_1"}' localhost:50051 acme.orders.v1.OrderService/GetOrder
grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check
(grpcurl needs reflection or -proto/-import-path flags.) Add tests asserting the
status code and error details for each failure branch, and one test that a cancelled
context stops the handler.
Output template
## gRPC change
- Package / service: <acme.orders.v1.OrderService>
- RPCs added or changed: <name, unary|streaming>
- buf lint / breaking: <pass | findings>
- Error mapping: <situation -> code>
- Deadlines and retries: <values, retryable codes>
- Rollout notes: <server first, then clients> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "gRPC Services"
short_description: "Protos that evolve, calls that always have deadlines"
default_prompt: "$grpc-services Add a ListOrders RPC with pagination to our OrderService proto" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.