protoc-gen-ts-temporal
A protoc plugin that emits a typed TypeScript Temporal
client from temporal.v1.*-annotated protos.
Status: pre-release (v0.0.0). The plugin works end-to-end against the
golden fixtures in crates/protoc-gen-ts-temporal/tests/fixtures/; distribution
(crates.io / GitHub Releases / BSR Remote Plugin) is Phase 4.
What you get
For each service annotated with temporal.v1.workflow / signal / query /
update options, the plugin emits two TypeScript files next to your normal
*_pb.ts:
<basename>_temporal.ts— typed client class, workflow name constants,defineSignal/defineQuery/defineUpdateexports, a*Runhandle class per workflow with typed signal / query / update methods, and free*With*Starthelpers when the workflow opts into signal-with-start or update-with-start.<basename>_pb_register.ts— exportsschemas: readonly DescMessage[]for every proto message reachable from the file's services. Feed the union of these arrays into@temporalio/common/lib/protobufs-es'sDefaultPayloadConverterWithProtobufsEs({ registry })at your data-converter site (the constructor accepts a schemas array directly).
Schema source
Annotations come from
cludden/protoc-gen-go-temporal,
published on BSR as buf.build/cludden/protoc-gen-go-temporal. We do not
ship our own annotation schema — a proto annotated for protoc-gen-go-temporal
generates a Go client (cludden), a Rust client (planned: protoc-gen-rust-temporal),
and a TS client (this plugin) with zero proto changes.
Wire format
Owned upstream by
@temporalio/common/lib/protobufs-es.
The plugin emits @bufbuild/protobuf v2 message types plus a
schemas: readonly DescMessage[] inventory per file; the SDK converter
handles serialization (binary + JSON), the messageType header, and
cross-language compatibility with the Go SDK.
Runtime targets
| Runtime | Tier |
|---|---|
| Bun | 1 |
| Deno ≥ 2.7.14 | 1 |
| Node | 2 |
Generator hygiene rules: explicit .ts extensions on relative imports, bare
specifiers for npm packages, no node: imports, no process.env.
Quickstart
Phase 4 distribution is not yet shipped. Until then, build the binary from this repo and point
buf/protocat it directly.
1. Add the annotation schema to your buf.yaml
version: v2
deps:
- buf.build/cludden/protoc-gen-go-temporal # annotation schema
- buf.build/temporalio/api # transitive enum deps
2. Annotate your service
syntax = "proto3";
package invoice.v1;
import "temporal/v1/temporal.proto";
message RunRequest { string id = 1; }
message RunResponse { string result = 1; }
service InvoiceBatch {
rpc Run(RunRequest) returns (RunResponse) {
option (temporal.v1.workflow) = {
task_queue: "invoice-generator"
};
}
}
3. Build the plugin and wire it into buf.gen.yaml
# binary lands at target/release/protoc-gen-ts-temporal
# buf.gen.yaml
version: v2
plugins:
- local: ./target/release/protoc-gen-ts-temporal
out: gen
This emits gen/invoice/v1/invoice_temporal.ts and
gen/invoice/v1/invoice_pb_register.ts.
4. Wire the data converter
// src/data-converter.ts
import { DefaultPayloadConverterWithProtobufsEs } from "@temporalio/common/lib/protobufs-es";
import { schemas as invoiceSchemas } from "./gen/invoice/v1/invoice_pb_register.ts";
export const payloadConverter = new DefaultPayloadConverterWithProtobufsEs({
registry: [...invoiceSchemas],
});
5. Use the generated client
import { Client } from "@temporalio/client";
import { InvoiceBatchClient } from "./gen/invoice/v1/invoice_temporal.ts";
import { payloadConverter } from "./src/data-converter.ts";
const client = new Client({ dataConverter: { payloadConverter } /* ... */ });
const batch = new InvoiceBatchClient(client);
const run = await batch.run({ id: "inv-123" });
const result = await run.result();
Plugin options
Pass via --ts-temporal_opt=k=v,k=v (or opt: in buf.gen.yaml):
| Key | Default | Effect |
|---|---|---|
pb_suffix |
_pb |
Sibling proto-types module suffix used by generated imports. Matches @bufbuild/protobuf-es out of the box; switch to _proto for ts-proto, etc. |
Unknown keys are an error so typos surface immediately.
# buf.gen.yaml
version: v2
plugins:
- local: ./target/release/protoc-gen-ts-temporal
out: gen
opt:
- pb_suffix=_proto
Annotation surface
| Annotation | What the plugin emits |
|---|---|
temporal.v1.workflow on a method |
Typed start(input, opts) method + a *Run handle class with typed result(). |
temporal.v1.query |
handle.<queryName>() returning the typed response. |
temporal.v1.signal |
handle.<signalName>(input). Signal output must be google.protobuf.Empty. |
temporal.v1.update |
handle.<updateName>(input) returning the typed response. |
temporal.v1.activity |
Validate only in v1 — used for name-collision detection. Worker-side activity codegen is out of scope here. |
WorkflowOptions.Signal.start = true |
Free function <signal>With<Workflow>Start(...) invoking client.workflow.signalWithStart. |
WorkflowOptions.Update.start = true |
Free function <update>With<Workflow>Start(...) invoking client.workflow.startUpdateWithStart. |
Out of scope for v1 emit: XNSActivityOptions, Patch, CLI*Options, FieldOptions.
Development
TEMPORAL_GOLDEN_BLESS=1
Fixtures live under crates/protoc-gen-ts-temporal/tests/fixtures/<name>/
with one .proto per fixture and the blessed *_temporal.ts +
*_pb_register.ts outputs alongside.
Roadmap
See SPEC.md. Phases 0–3 are shipped (scaffold, parse, render,
end-to-end golden fixtures). Phase 4 (distribution) and Phase 5
(invoice-generator migration as the first external consumer) are next.
License
MIT. See LICENSE.