{"id":50290363,"url":"https://github.com/parseablehq/temporal-plugin","last_synced_at":"2026-05-28T05:34:20.598Z","repository":{"id":355355869,"uuid":"1227704001","full_name":"parseablehq/temporal-plugin","owner":"parseablehq","description":"Parseable plugin for Temporal","archived":false,"fork":false,"pushed_at":"2026-05-15T00:37:06.000Z","size":93,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-28T05:34:11.878Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/parseablehq.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["parseable","operator"]}},"created_at":"2026-05-03T03:40:31.000Z","updated_at":"2026-05-15T00:37:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/parseablehq/temporal-plugin","commit_stats":null,"previous_names":["parseablehq/temporal-plugin"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/parseablehq/temporal-plugin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parseablehq%2Ftemporal-plugin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parseablehq%2Ftemporal-plugin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parseablehq%2Ftemporal-plugin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parseablehq%2Ftemporal-plugin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/parseablehq","download_url":"https://codeload.github.com/parseablehq/temporal-plugin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parseablehq%2Ftemporal-plugin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33596316,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-28T05:34:17.592Z","updated_at":"2026-05-28T05:34:20.593Z","avatar_url":"https://github.com/parseablehq.png","language":"TypeScript","funding_links":["https://github.com/sponsors/parseable","https://github.com/sponsors/operator"],"categories":[],"sub_categories":[],"readme":"# @parseable/temporal\n\nTemporal middleware plugin that ships workflow and activity execution events to [Parseable](https://www.parseable.com/) as OpenTelemetry logs and traces.\n\n\u003e **End-user integration guide:** [INTEGRATION.md](./INTEGRATION.md) - install, configure, schema reference, query examples.\n\u003e\n\u003e **Submission status:** [STATUS.md](./STATUS.md) - what's done and what's pending for the Temporal AI Partner Program submission.\n\nThe plugin emits structured logs (workflow/activity start, complete, fail, retry, duration) into a Parseable log stream, alongside OpenTelemetry traces (`RunWorkflow:*`, `StartActivity:*`, `RunActivity:*`) into a Parseable trace stream. Users get a flat queryable schema for analytics plus a waterfall view of workflow execution.\n\nThis README is the developer-facing landing page for the repo (architecture, repo layout, how to run the demo, caveats). For end-user consumption see `INTEGRATION.md`.\n\n---\n\n## Repository layout\n\n`src/` is the publishable plugin package (`@parseable/temporal`). `examples/` contains a runnable demo worker and clients. `test/` contains mocha tests against the plugin using the demo workflows as fixtures.\n\n```\nsrc/                              # the integration - published as @parseable/temporal\n├── index.ts                      # ParseablePlugin class (extends SimplePlugin)\n├── activity-interceptor.ts       # ActivityInbound interceptor (worker process)\n├── workflow-interceptor.ts       # WorkflowInbound + Outbound interceptors (workflow isolate, replay-safe via sinks)\n├── workflow.ts                   # public workflowEvent() helper (consumer imports from @parseable/temporal/workflow)\n├── exporters.ts                  # OTLP HTTP exporters (logs + traces) + SanitizingSpanExporter\n├── version.ts                    # PLUGIN_VERSION constant\n└── types.ts                      # ParseableEventRecord schema\n\nexamples/                         # runnable demo - not published\n├── activities.ts                 # greet (success), chargeCard (always fails)\n├── workflows.ts                  # example, failingExample, userEventExample, parentExample, signalEventExample, queryUpdateExample, childSignalParent, continueAsNewExample, updateFailureExample, parentFailingExample\n├── worker.ts                     # demo worker wired up with ParseablePlugin\n├── client.ts                     # triggers happy-path workflow\n├── fail-client.ts                # triggers failing workflow\n├── event-client.ts               # triggers user-event workflow\n└── parent-client.ts              # triggers parent → child workflow\n\ntest/                             # mocha tests (use examples/* as fixtures)\n├── replay-safety.test.ts         # interceptor coverage: workflow/activity/signal/query/update/child_workflow/continue_as_new + failure paths, asserts zero emissions during history replay\n├── activities.test.ts            # unit test for greet activity\n├── workflows.test.ts             # workflow integration test (requires TestWorkflowEnvironment)\n└── workflows-mocks.test.ts       # workflow integration test with mocked activities\n```\n\n---\n\n## Architecture\n\n```\n                        ┌───────────────────┐\n                        │  Temporal Server  │\n                        │ (localhost:7233)  │\n                        └─────────┬─────────┘\n                                  │ gRPC\n                  ┌───────────────┴───────────────┐\n                  │           Worker              │\n                  │                               │\n                  │  ┌─────────────────────────┐  │\n                  │  │  Workflow V8 isolate    │  │  ← replay-safe; cannot do I/O\n                  │  │                         │  │\n                  │  │  WorkflowInbound +      │  │\n                  │  │  WorkflowOutbound       │  │\n                  │  │  interceptors           │  │\n                  │  │                         │  │\n                  │  │  proxySinks ──────┐     │  │\n                  │  └───────────────────┼─────┘  │\n                  │                      ▼        │\n                  │  ┌──────────────────────────┐ │\n                  │  │ Sink consumer (worker proc)│\n                  │  │ enriches with service_name│\n                  │  └──────────────┬───────────┘ │\n                  │                 │             │\n                  │  ┌──────────────▼───────────┐ │\n                  │  │ ActivityInbound          │ │\n                  │  │ interceptor              │ │\n                  │  └──────────────┬───────────┘ │\n                  │                 │             │\n                  │  ┌──────────────▼───────────┐ │\n                  │  │ emit(record)             │ │\n                  │  │  → OTel Logger           │ │\n                  │  │  → BatchLogRecordProc    │ │\n                  │  │  → OTLPLogExporter       │ │\n                  │  └──────────────┬───────────┘ │\n                  │                 │             │\n                  │  ┌──────────────┴────────────┐│\n                  │  │ Temporal OpenTelemetryPlug││\n                  │  │  → BatchSpanProcessor     ││\n                  │  │  → SanitizingSpanExporter ││\n                  │  │  → OTLPTraceExporter      ││\n                  │  └──────────────┬────────────┘│\n                  └─────────────────┼─────────────┘\n                                    │ HTTPS\n                          ┌─────────▼──────────┐\n                          │  Parseable         │\n                          │  /v1/logs   (logs) │\n                          │  /v1/traces (spans)│\n                          └────────────────────┘\n```\n\n### Key design points\n\n- **Replay safety.** Workflow events are emitted via `proxySinks` with `callDuringReplay: false`. When Temporal replays a workflow's history (after a worker crash, cache eviction, or manual replay), the sink is skipped - no duplicate logs or spans. Verified by `src/mocha/replay-safety.test.ts`.\n- **Two layers, one plugin.** `ParseablePlugin` extends `@temporalio/plugin`'s `SimplePlugin` and internally composes Temporal's official `OpenTelemetryPlugin` for trace emission. Logs are emitted from our own interceptors directly to OTel's log API. Both flow into Parseable through OTLP/HTTP.\n- **`SanitizingSpanExporter`.** Temporal's OTel plugin emits spans with nested objects, `Date` instances, and `undefined` fields as attributes. OTLP attribute values are restricted to primitives or arrays of primitives, so Parseable's strict OTLP parser rejects the raw payload with `400 Invalid data for Value`. The sanitizer wraps the trace exporter and flattens nested objects to JSON strings, `Date` to ISO, and drops `undefined`s before serialization.\n- **OTel pinned to 1.x.** Temporal's `OpenTelemetryPlugin` pins `@opentelemetry/sdk-trace-base@^1.25.1`. The OTel ecosystem has split between 1.x (mature) and 2.x (newer). We ride the 1.x line - `sdk-trace-base@1.30.x`, `resources@1.30.x`, `exporter-{logs,trace}-otlp-http@0.57.x`, `sdk-logs@0.57.x` - until Temporal moves.\n\n---\n\n## Running the demo locally\n\n### Prerequisites\n\n- Node.js 20+\n- [Temporal CLI](https://github.com/temporalio/cli) (`brew install temporal` on macOS)\n- A Parseable instance reachable on the network. For dev: a local instance with default credentials.\n\n### Three terminals\n\n**Terminal 1 - Temporal dev server:**\n\n```bash\ntemporal server start-dev\n```\n\nRuns on `localhost:7233` (gRPC) and `http://localhost:8233` (UI).\n\n**Terminal 2 - Worker:**\n\n```bash\nnpm install\nPARSEABLE_URL=https://your-parseable-host \\\nPARSEABLE_USERNAME=youruser \\\nPARSEABLE_PASSWORD=yourpass \\\nnpm run examples:worker.watch\n```\n\n`PARSEABLE_URL` is required - the worker refuses to start without it. Username/password default to `admin/admin` if unset (matching a default Parseable dev install). The worker connects to Temporal at `localhost:7233`, polls the `hello-world` task queue, and auto-restarts on `src/` or `examples/` changes via nodemon.\n\n**Terminal 3 - Client (run on demand):**\n\n```bash\nnpm run examples:workflow         # success path: greet activity\nnpm run examples:workflow:fail    # failure path: chargeCard with 3-retry policy\nnpm run examples:workflow:event   # user-event path: workflow emits custom domain events via workflowEvent()\nnpm run examples:workflow:parent  # parent → child workflow path: exercises the outbound interceptor\n```\n\nAfter running, check Parseable at `${PARSEABLE_URL}`:\n- Stream `temporal-logs` - workflow/activity records with attributes `workflow_id`, `activity_name`, `attempt`, `status`, `duration_ms`, `service_name`, etc.\n- Stream `temporal-traces` - spans `RunWorkflow:example`, `StartActivity:greet`, `RunActivity:greet`.\n\n---\n\n## Tests\n\n```bash\nnpm test                                                                        # runs all mocha tests\nnpx mocha --require ts-node/register test/replay-safety.test.ts                 # run only replay-safety\n```\n\nThe replay-safety suite exercises every interceptor path and asserts that replay re-emits **zero** records (sinks correctly skipped via `callDuringReplay: false`, activities and queries don't re-execute on replay):\n\n| Test | Effects covered | Live invariants asserted |\n| --- | --- | --- |\n| `example` + `greet` | workflow inbound, activity | 2 workflow records (started+completed), 2 activity records |\n| `signalEventExample` | `handleSignal`, `workflowEvent` | 2 signal records, 2 user_event records |\n| `queryUpdateExample` | `handleQuery`, `handleUpdate` | 2 query records, 2 update records |\n| `childSignalParent` | `startChildWorkflowExecution`, `signalWorkflow` outbound | child_workflow start+complete (after child finishes, not at start RPC), 2 outbound signal records |\n| `continueAsNewExample` | `continueAsNew` outbound | single record with `status: 'started'` only |\n| `failingExample` | retries + workflow failure | 3 activity started + 3 activity failed (with `attempt` 1/2/3 and `duration_ms`/`error`), 1 failed workflow record |\n| `updateFailureExample` | update handler `ApplicationFailure` | 1 update started + 1 update failed (no completed), error propagated |\n| `parentFailingExample` | child-workflow run-time failure | outbound child_workflow started + failed, parent workflow failed |\n\nEach test fetches the workflow history, replays it via `Worker.runReplayHistory()` with a fresh plugin, and re-asserts that the record stream is empty for the effect under test.\n\nThis requires a running Temporal dev server (`temporal server start-dev`) - the suite connects to `localhost:7233` rather than spinning up an in-process test server.\n\n---\n\n## Caveats\n\n- **OTel ecosystem version split.** We pin to OTel 1.x because Temporal's plugin does. When Temporal moves to 2.x, we follow.\n- **Empty-body warning on OTLP success.** Parseable returns HTTP 200 with an empty body for accepted OTLP payloads. OTel's deserializer logs `Export succeeded but could not deserialize response - is the response specification compliant?` - this is benign and only visible at `DiagLogLevel.DEBUG` or above.\n- **Span attribute sanitization.** The `SanitizingSpanExporter` is a workaround for an interop gap between Temporal's OTel plugin (emits non-primitive span attributes) and strict OTLP parsers (require primitive attribute values). Without it, Parseable returns `400 Invalid data for Value`.\n- **Throw `ApplicationFailure` for graceful handler failures.** Signal/update handlers that throw a plain `Error` are treated by Temporal as a workflow-task failure: the task is retried until it succeeds, and the plugin will emit one `started`+`failed` record pair per retry. To fail an update (or any handler) cleanly without retry storms, throw `ApplicationFailure.create({ message, nonRetryable: true })` from `@temporalio/workflow`. The interceptor then records exactly one `failed` event and the error propagates to the client as an update failure rather than a task failure.\n- **`child_workflow` completion is tracked from the child, not the start RPC.** The outbound interceptor wraps the result promise returned by `next(input)` so `status: 'completed'` (or `failed`) fires when the child actually finishes - not when the start call returns. Start-time RPC errors and run-time child failures are reported with distinct `failed` records.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fparseablehq%2Ftemporal-plugin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fparseablehq%2Ftemporal-plugin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fparseablehq%2Ftemporal-plugin/lists"}