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.

  • Backend & APIs
  • $grpc-services
  • 260 lines
  • 2 files
  • ~2,250 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- grpc-services

Installs to ~/.agents/skills/grpc-services/ for every project on this machine.

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

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.

~/.agents/skills/grpc-services/SKILL.md

--- name: grpc-services description: Designs and implements gRPC services with Protocol Buffers - package and message layout, field numbering and evolution, buf lint and breaking checks,… license: MIT ---

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 for buf.yaml, buf.gen.yaml, proto/ trees, or protoc invocations 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/Delete methods, 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.Empty as 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 FieldMask so clients cannot accidentally blank fields they did not send.
  • Money as integer minor units plus currency (or google.type.Money), never double.

3. Evolution rules

ChangeSafe on the wire?
Add a field with a new numberYes
Rename a field (same number and type)Wire-safe, breaks JSON and generated code
Remove a fieldOnly if you reserved its number and name
Change a field’s type or numberNo
Reuse an old field numberNever; it corrupts data from old clients
Add an enum valueYes; old clients see it as unknown/zero
Move fields into a new oneofNo

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...Server struct 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 protovalidate rules if the project uses them) and return INVALID_ARGUMENT with field details.

5. Errors, deadlines, retries

Status code decision table:

SituationCodeClient may retry?
Bad input, independent of system stateINVALID_ARGUMENTNo
Entity missingNOT_FOUNDNo
Create of something that existsALREADY_EXISTSNo
System not in required state (e.g. order already shipped)FAILED_PRECONDITIONNo, until state changes
Concurrency conflict, transaction abortedABORTEDYes, at a higher level
No or bad credentialsUNAUTHENTICATEDNo
Authenticated but not allowedPERMISSION_DENIEDNo
Quota or rate limit hitRESOURCE_EXHAUSTEDYes, with backoff
Transient: server shutting down, dependency downUNAVAILABLEYes, with backoff
Deadline passedDEADLINE_EXCEEDEDOnly if idempotent
Bug, invariant brokenINTERNALNo
Method not implementedUNIMPLEMENTEDNo

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

NeedShape
Request/response under a few MBUnary
Server pushes updates or a large result setServer streaming
Client uploads many items, one summary backClient streaming
Interactive, both sides send independentlyBidirectional

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_robin against 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.Health and use it for readiness probes (Kubernetes supports native grpc probes).
  • 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 int32 to int64 is wire-compatible, but old clients silently truncate values outside the 32-bit range and generated code changes; treat it as breaking.
  • Proto3 scalars without optional cannot distinguish 0/"" from unset; PATCH-style updates silently zero fields unless a field mask is used.
  • Retrying non-idempotent RPCs on DEADLINE_EXCEEDED double-applies writes.
  • Enabling server reflection in production exposes the full API surface; gate it.
  • Keepalive pings more frequent than the server permits trigger GOAWAY with too_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.yamlgenerated from this page

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.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem