@trigger.dev/convex 4.8.3 → 4.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,12 @@ Use Convex for application state and Trigger.dev for work that needs longer exec
6
6
 
7
7
  **Experimental MVP.** No HTTP routes, webhooks, Trigger lifecycle hooks, or changes to your Trigger server are required.
8
8
 
9
+ ## Example
10
+
11
+ [Try the live demo](https://ardent-rabbit-965.convex.site).
12
+
13
+ The [image-editing example](https://github.com/triggerdotdev/trigger.dev/tree/main/packages/convex/example) demonstrates private image processing, Realtime progress, and Convex synchronization. It uses Trigger.dev `4.8.3` from npmjs and runs as a standalone pnpm workspace. The example is not included in npm packages or component builds.
14
+
9
15
  ## Install
10
16
 
11
17
  ```sh
@@ -119,6 +125,21 @@ const run = useQuery(api.jobs.status, { jobId });
119
125
 
120
126
  The ID returned by `trigger.trigger` identifies the **component record**, not the Trigger.dev run. The latter becomes available as `run.runId` after submission.
121
127
 
128
+ ## Agent skill
129
+
130
+ The package ships a versioned [`trigger-convex` skill](https://github.com/triggerdotdev/trigger.dev/blob/main/packages/convex/skills/trigger-convex/SKILL.md) covering transactional triggering, owned reactive queries, callbacks, Realtime + refresh, reload recovery, and token safety.
131
+
132
+ Ask your coding assistant to read `node_modules/@trigger.dev/convex/skills/trigger-convex/SKILL.md`, or copy it into your assistant's native skill directory. For example, from a project with this package installed:
133
+
134
+ ```sh
135
+ mkdir -p .agents/skills/trigger-convex
136
+ cp node_modules/@trigger.dev/convex/skills/trigger-convex/SKILL.md .agents/skills/trigger-convex/SKILL.md
137
+ ```
138
+
139
+ Use `.claude/skills/`, `.cursor/skills/`, or `.github/skills/` instead if appropriate for your assistant. Re-copy after upgrading `@trigger.dev/convex`; copied files do not update automatically. For non-hoisted installs, locate the package with `node -p "require.resolve('@trigger.dev/convex/package.json')"`.
140
+
141
+ This skill is installed manually, not through `npx trigger.dev@latest skills`. Older package versions may not contain it.
142
+
122
143
  ## API
123
144
 
124
145
  - `trigger(ctx, taskId, payload, options?)`: enqueue work. Calling from a mutation makes the request atomic with your application's writes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trigger.dev/convex",
3
- "version": "4.8.3",
3
+ "version": "4.8.4",
4
4
  "description": "Run Trigger.dev tasks from Convex mutations and query their results reactively",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -15,7 +15,9 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "src/component",
18
- "!src/component/**/*.test.ts"
18
+ "skills/*/SKILL.md",
19
+ "!src/component/**/*.test.ts",
20
+ "!example/**"
19
21
  ],
20
22
  "exports": {
21
23
  ".": {
@@ -0,0 +1,264 @@
1
+ ---
2
+ name: trigger-convex
3
+ description: >
4
+ Integrates Trigger.dev background tasks with Convex using @trigger.dev/convex.
5
+ Use when triggering work transactionally from a Convex mutation, showing reactive
6
+ run state, handling completion callbacks, combining Trigger Realtime with Convex,
7
+ restoring jobs after reload, or repairing paused synchronization. Covers ownership,
8
+ idempotency, scoped browser tokens, and credential-safe task payloads. Not for
9
+ Convex-only applications or Trigger.dev applications without Convex.
10
+ license: Apache-2.0
11
+ ---
12
+
13
+ # Trigger.dev with Convex
14
+
15
+ ## Check the installed contract first
16
+
17
+ Locate `@trigger.dev/convex/package.json`, then read the adjacent `README.md` and
18
+ `dist/client/index.d.ts`. In a non-hoisted installation, resolve the package with:
19
+
20
+ ```bash
21
+ node -p "require.resolve('@trigger.dev/convex/package.json')"
22
+ ```
23
+
24
+ Use the installed declarations as authority. Older releases do not expose
25
+ `refresh()`. If it is absent, use durable polling or upgrade to a compatible
26
+ release; do not invent a replacement method or accept browser-supplied run state.
27
+ Keep the application's Convex version compatible with the package's peer dependency.
28
+ Do not pin an arbitrary preview URL unless the user explicitly needs that preview.
29
+
30
+ Choose the relevant flow:
31
+
32
+ | Request | Approach |
33
+ | --- | --- |
34
+ | Enqueue a report, render, import, or AI task | Call `trigger.trigger` inside the application's authenticated mutation. |
35
+ | Display durable status/results | Authorize the application's job, then call `trigger.getRun` from a query. |
36
+ | Update application records on completion | Register a short internal mutation using `vRun`. |
37
+ | Display responsive browser progress | Subscribe to Trigger Realtime; request an authorized Convex refresh on terminal events. |
38
+ | Restore an edit after reload or leaving the page | Persist the selected application job/request ID and inputs; recover owned work from Convex. |
39
+ | Recover paused status synchronization | Repair credentials/callback failures, then call authorized `resume`; do not enqueue another task. |
40
+
41
+ ## Install and configure
42
+
43
+ Install `@trigger.dev/convex` and `convex`. Deploy the actual task separately with
44
+ Trigger.dev; the component does not deploy task code. Keep `TRIGGER_SECRET_KEY`
45
+ only in the Convex server environment, for the intended Trigger environment.
46
+
47
+ ```typescript
48
+ // convex/convex.config.ts
49
+ import { defineApp } from "convex/server";
50
+ import triggerComponent from "@trigger.dev/convex/convex.config.js";
51
+
52
+ const app = defineApp();
53
+ app.use(triggerComponent);
54
+ export default app;
55
+ ```
56
+
57
+ Run `npx convex dev` to generate the application's component references.
58
+
59
+ ```typescript
60
+ // convex/trigger.ts
61
+ import { Trigger } from "@trigger.dev/convex";
62
+ import { components } from "./_generated/api";
63
+
64
+ export const trigger: Trigger = new Trigger(components.trigger);
65
+ ```
66
+
67
+ Constructor options are `secretKey`, `baseUrl`, and `onComplete`. The default API
68
+ is `https://api.trigger.dev`; `TRIGGER_API_URL` is not automatically read by the
69
+ component. Set `baseUrl` explicitly for a different API host. HTTP is allowed only
70
+ for localhost development. Keep server keys out of `VITE_`/`NEXT_PUBLIC_` variables.
71
+
72
+ ## Enqueue atomically and authorize reads
73
+
74
+ Maintain an application-owned job table. The component has no user/tenant model.
75
+ This example assumes an existing deployed `process-text` task that accepts `{ text }`.
76
+ Validate business limits and authorize any referenced documents/files before enqueueing.
77
+
78
+ ```typescript
79
+ // convex/schema.ts
80
+ import { defineSchema, defineTable } from "convex/server";
81
+ import { v } from "convex/values";
82
+
83
+ export default defineSchema({
84
+ jobs: defineTable({
85
+ owner: v.string(),
86
+ requestId: v.string(),
87
+ text: v.string(),
88
+ componentRunId: v.optional(v.string()),
89
+ }).index("by_owner_request", ["owner", "requestId"])
90
+ .index("by_componentRunId", ["componentRunId"]),
91
+ });
92
+ ```
93
+
94
+ ```typescript
95
+ // convex/jobs.ts
96
+ import { vRun } from "@trigger.dev/convex";
97
+ import { v } from "convex/values";
98
+ import { mutation, query } from "./_generated/server";
99
+ import { trigger } from "./trigger";
100
+
101
+ export const request = mutation({
102
+ args: { text: v.string(), requestId: v.string() },
103
+ returns: v.id("jobs"),
104
+ handler: async (ctx, args) => {
105
+ const user = await ctx.auth.getUserIdentity();
106
+ if (!user) throw new Error("Unauthorized");
107
+ const owner = user.tokenIdentifier;
108
+ const existing = await ctx.db.query("jobs")
109
+ .withIndex("by_owner_request", q => q.eq("owner", owner).eq("requestId", args.requestId))
110
+ .unique();
111
+ if (existing) {
112
+ if (existing.text !== args.text) throw new Error("Request ID reused for different input");
113
+ return existing._id;
114
+ }
115
+ const jobId = await ctx.db.insert("jobs", { owner, ...args });
116
+ const componentRunId = await trigger.trigger(ctx, "process-text", { text: args.text }, {
117
+ idempotencyKey: `process-text:${owner}:${args.requestId}`,
118
+ });
119
+ await ctx.db.patch(jobId, { componentRunId });
120
+ return jobId;
121
+ },
122
+ });
123
+
124
+ export const status = query({
125
+ args: { jobId: v.id("jobs") },
126
+ returns: v.union(vRun, v.null()),
127
+ handler: async (ctx, { jobId }) => {
128
+ const user = await ctx.auth.getUserIdentity();
129
+ const job = await ctx.db.get(jobId);
130
+ if (!user || !job || job.owner !== user.tokenIdentifier) return null;
131
+ return job.componentRunId ? trigger.getRun(ctx, job.componentRunId) : null;
132
+ },
133
+ });
134
+ ```
135
+
136
+ Preserve the same request ID when retrying an unconfirmed submission. Namespace
137
+ component idempotency keys by tenant/owner, task, and logical request. They dedupe
138
+ across the entire component instance, not just a task, and do not compare payloads.
139
+ An application mutation rollback also rolls back the component enqueue/scheduling.
140
+ Do not replace this with a direct SDK trigger from an action if atomicity is required.
141
+ Use type-only task imports when adding task payload inference; do not bundle task
142
+ implementations, Sharp, or provider clients into the Convex mutation runtime.
143
+
144
+ There are three distinct identifiers:
145
+
146
+ - Application `jobId`: accept this from the browser and enforce ownership.
147
+ - Component `run.id`: returned by `trigger.trigger`; pass it to `getRun`, `refresh`, and `resume`.
148
+ - Remote `run.runId`: available after submission; subscribe to this with Trigger Realtime.
149
+
150
+ For React, use an application query such as `useQuery(api.jobs.status, { jobId })`.
151
+ Only persist/display results verified by Convex. `getRun` omits stored credentials
152
+ and request payloads, but its output, errors, and metadata can still contain private data.
153
+ Do not expose `listRuns` as a global client-visible feed; use indexed owned jobs and
154
+ bounded lookups instead.
155
+
156
+ ## Apply completion to application state
157
+
158
+ When an application record must change on completion, extend its schema with the
159
+ task-specific status/result fields, then add an internal mutation with
160
+ `args: { run: vRun }` and `returns: v.null()`. Find its job through the
161
+ `by_componentRunId` index using `run.id`, validate the task's output, and update it.
162
+ Configure the client with `onComplete: internal.jobs.complete`; keep the explicit
163
+ `Trigger` type annotation to avoid circular inference through the generated API.
164
+
165
+ Keep the callback short and transactional. It runs with the component's terminal
166
+ update; throwing rolls both back and schedules a retry. Do network I/O in separate
167
+ actions, not in the callback mutation. A callback is optional for reactive reads.
168
+ Treat omitted output and expiring output URLs explicitly rather than assuming every
169
+ completed run has an inline result.
170
+
171
+ ## Combine Realtime progress with authoritative Convex state
172
+
173
+ Install `@trigger.dev/react-hooks` in a React frontend and `@trigger.dev/sdk` in the
174
+ server that mints tokens. These are separate from the component-only polling setup.
175
+
176
+ Only use the following wrapper if the installed component exposes `refresh()`:
177
+
178
+ ```typescript
179
+ // Add to convex/jobs.ts
180
+ export const refresh = mutation({
181
+ args: { jobId: v.id("jobs") },
182
+ returns: v.null(),
183
+ handler: async (ctx, { jobId }) => {
184
+ const user = await ctx.auth.getUserIdentity();
185
+ const job = await ctx.db.get(jobId);
186
+ if (!user || !job || job.owner !== user.tokenIdentifier) throw new Error("Unauthorized");
187
+ if (job.componentRunId) await trigger.refresh(ctx, job.componentRunId);
188
+ return null;
189
+ },
190
+ });
191
+ ```
192
+
193
+ 1. Authenticate a server-side token request and verify the application job's owner.
194
+ Derive its remote run ID on the server; never grant access to a caller-chosen run.
195
+ 2. Mint a short-lived `auth.createPublicToken` scoped to `read: { runs: [runId] }`.
196
+ Use an SDK-capable server runtime, such as a Convex Node action, and scoped
197
+ `auth.withAuth` configuration rather than mutating shared global credentials.
198
+ The SDK spells its API host option `baseURL`, unlike the component's `baseUrl`.
199
+ Match the key/host environment, bound expiry to the application's access lifetime,
200
+ and return only the token, remote ID, expiry, and required public connection URL.
201
+ 3. Subscribe with `useRealtimeRun` or `runs.subscribeToRun` only after the remote ID
202
+ exists. Limit the browser to the selected owned run, renew credentials through
203
+ the authorized endpoint, and clean up on job/session changes. Reject late token
204
+ responses and metadata for a previous selection.
205
+ 4. On completion, including an already-terminal snapshot after reconnecting, call
206
+ the owned `refresh` mutation. Include failed, canceled, expired, and timed-out
207
+ runs. Do not refresh on every metadata update or retry a failed effect in a loop.
208
+ 5. Observe the Convex query for persisted status/results. A refresh returns `null`
209
+ after scheduling; it does not wait for completion or return a new run snapshot.
210
+ Keep scheduled polling/watchdog recovery enabled when the browser goes away.
211
+
212
+ Refresh hints are coalesced per run, with refresh-driven checks at least five seconds
213
+ after the previous status check. An in-flight hint queues a follow-up without
214
+ invalidating that observation. Pending submissions, paused synchronization, and
215
+ completed records are no-ops; refresh neither submits work nor resumes paused work.
216
+
217
+ **Token safety:** a run-read token also authorizes REST reads of its payload.
218
+ Realtime `skipColumns: ["payload", "output"]` reduces streamed data; it does not
219
+ restrict REST access. Remove worker keys, upload capabilities, and other credentials
220
+ from token-readable payloads, outputs, metadata, and errors before issuing browser
221
+ tokens. Do not mint tokens for legacy runs that still contain credentials; deploying
222
+ new code or changing column filters does not rewrite old remote payloads. Derive worker authorization server-side or use server-only signing instead.
223
+ Authorize private artifact delivery separately; a public run token is not a file
224
+ access policy. Keep browser run tokens in memory and never log credentials.
225
+
226
+ Persist the selected application job ID, draft inputs, and unconfirmed request ID
227
+ before sending a request. Restore owned active jobs from Convex after reload;
228
+ otherwise restore only explicit selections, not any old result with matching defaults.
229
+ Use actual task metadata for phase progress, explain disabled submission states,
230
+ and do not publish results solely because Realtime reports completion.
231
+
232
+ ## Troubleshoot without duplicating work
233
+
234
+ | Symptom | Fix |
235
+ | --- | --- |
236
+ | Duplicate jobs after a lost response | Reuse the logical request ID and query the owned job before enqueueing again. |
237
+ | Wrong run or a null remote ID | Distinguish application/component/remote IDs; wait for confirmed submission before subscribing. |
238
+ | Browser refresh resolves but results lag | Observe Convex; refresh is asynchronous, coalesced, and rate-limited. |
239
+ | Component/validator types are incompatible | Check for duplicate Convex installations or peer contexts; align component/consumer dependencies rather than hiding mismatches with casts. |
240
+ | `refresh` is missing | Check installed declarations; upgrade deliberately or retain polling, not a fabricated API. |
241
+ | `syncStatus === "paused"` | Inspect `syncError`, repair credentials/callbacks, then authorize `resume(ctx, componentId)`. |
242
+ | Old work disappears after reload | Persist selection/inputs/request ID; recover owned active work instead of resetting to defaults. |
243
+ | Token leaks worker access | Remove credentials from all readable run fields; column filtering cannot repair this. |
244
+ | Data from another tenant appears | Authorize every query/mutation/token/artifact request; never trust arbitrary IDs or shared tags. |
245
+ | Successful remote work never commits | Check callback errors and output validation; retries roll back application completion too. |
246
+ | Large or missing results | Use storage references; inspect `outputOmitted`/`outputPresignedUrl` and refresh expired URLs server-side. |
247
+ | Run exists in a different environment | Align API host/key; resume does not migrate remote runs between environments. |
248
+
249
+ Polling normally backs off from about 15 to 60 seconds. Account for Convex executions
250
+ and Trigger API requests at scale; use Realtime for responsive UI, not rapid polling.
251
+ Payload plus trigger options must fit within 256 KiB, as must each inline output,
252
+ error, and metadata value. Component records are retained; paused requests retain
253
+ credentials and payloads for recovery, so plan application retention separately.
254
+ Remote submission uses a 24-hour idempotency TTL, but automatic submission retries
255
+ stop within one hour. After that window, reconcile remotely before creating new work;
256
+ do not claim unlimited exactly-once execution or blindly retry expensive tasks.
257
+
258
+ ## Verify the integration
259
+
260
+ Check mutation rollback, duplicate request IDs, cross-owner denial, callback failures,
261
+ and reconnecting to an already-terminal run. Verify that leaving the page does not
262
+ prevent durable completion. If tokens are issued, test cross-run denial and inspect
263
+ the full REST-readable payload for credentials. Add focused integration coverage;
264
+ do not spend on real provider calls or deploy without the user's authorization.