spinny:~/writing $ vim trigger-dev-background-jobs-guide.md
1~2Οι περισσότερες εφαρμογές παραγωγής χρειάζονται εργασία που δεν χωράει στον κύκλο request/response: αποστολή email, επεξεργασία uploads, εκτέλεση pipelines AI, συγχρονισμός δεδομένων τρίτων, δημιουργία αναφορών. Η παραδοσιακή απάντηση είναι μια ουρά (Redis, SQS, RabbitMQ), ένας στόλος workers, ένας scheduler και ένας εύθραυστος σωρός κώδικα συγκόλλησης που σπάει σε κάθε deploy.3~4[Trigger.dev](https://trigger.dev) συμπυκνώνει αυτό το stack σε ένα μόνο TypeScript SDK. Γράφετε συναρτήσεις, τις καλείτε από οπουδήποτε και η πλατφόρμα χειρίζεται queueing, retries, observability, scheduling και ανθεκτική εκτέλεση. Τα tasks τρέχουν όσο χρειάζεται - χωρίς timeout serverless 10 δευτερολέπτων, χωρίς χαμένη εργασία στα redeploys.5~6## Γιατί Trigger.dev7~8Η αλλαγή το 2026 είναι η ανθεκτική εκτέλεση. Οι ροές εργασίας πρέπει να επιβιώνουν από επανεκκινήσεις, crashes, deploys και rate limits. Πρέπει επίσης να streamάρουν την πρόοδο στο UI σε πραγματικό χρόνο και να σταματούν για εισαγωγή ανθρώπου. Το Trigger.dev ξαναχτίστηκε γύρω από αυτές τις απαιτήσεις με την έκδοση 3 και συνεχίζει να επεκτείνει την επιφάνεια υποδομής AI.9~10```mermaid11graph LR12 App[Η εφαρμογή σας] -->|trigger| API[Trigger.dev API]13 API --> Queue[Ανθεκτική Queue]14 Queue --> Worker[Worker Container]15 Worker -->|run task| Task[Κώδικας Task σας]16 Task -->|metadata| Realtime[Realtime Stream]17 Realtime --> UI[React UI]18 Worker --> Storage[Run State Store]19```20~21Το μοντέλο είναι απλό: ορίζετε tasks ως exports, το SDK τα παίρνει, η πλατφόρμα τα προγραμματίζει και τα τρέχει σε απομονωμένα containers και η κατάσταση run διατηρείται ώστε να μπορείτε να συνεχίσετε, να επαναλάβετε και να παρακολουθήσετε.22~23## Ξεκινώντας24~25### Αρχικοποίηση έργου26~27```bash28npx trigger.dev@latest login29npx trigger.dev@latest init30```31~32Αυτό δημιουργεί ένα αρχείο `trigger.config.ts` και έναν κατάλογο `trigger/` με tasks παραδειγμάτων. Το αρχείο config είναι η πηγή αλήθειας για το έργο σας: ποιοι κατάλογοι περιέχουν tasks, ρυθμίσεις build, lifecycle hooks και επιλογές runtime.33~34```typescript35// trigger.config.ts36import { defineConfig } from "@trigger.dev/sdk";37~38export default defineConfig({39 project: "proj_abc123",40 runtime: "node",41 logLevel: "log",42 maxDuration: 3600,43 retries: {44 enabledInDev: true,45 default: {46 maxAttempts: 3,47 factor: 2,48 minTimeoutInMs: 1000,49 maxTimeoutInMs: 30_000,50 },51 },52 dirs: ["./trigger"],53});54```55~56### Εκτέλεση tasks τοπικά57~58```bash59npx trigger.dev@latest dev60```61~62Ο dev server συνδέεται με το cloud, καταχωρεί τα tasks σας και κάνει stream τα runs μέσω του τοπικού σας κώδικα. Βάζετε breakpoints στον editor σας και τα χτυπάτε σε πραγματικά triggers - το ίδιο loop που θα χρησιμοποιούσατε σε οποιοδήποτε κανονικό έργο Node.js.63~64## Ορισμός Task65~66Ένα task είναι ένα αντικείμενο που εξάγεται με ένα μοναδικό `id` και μια συνάρτηση `run`. Το SDK επιθεωρεί τα exports σε όλο το `dirs` και τα καταχωρεί αυτόματα.67~68```typescript69// trigger/send-welcome-email.ts70import { task } from "@trigger.dev/sdk";71import { Resend } from "resend";72~73const resend = new Resend(process.env.RESEND_API_KEY);74~75export const sendWelcomeEmail = task({76 id: "send-welcome-email",77 retry: {78 maxAttempts: 5,79 factor: 1.8,80 minTimeoutInMs: 500,81 maxTimeoutInMs: 30_000,82 },83 run: async (payload: { email: string; name: string }) => {84 const { data, error } = await resend.emails.send({85 from: "hello@spinny.dev",86 to: payload.email,87 subject: `Welcome, ${payload.name}`,88 html: `<p>Glad you are here, ${payload.name}.</p>`,89 });90~91 if (error) throw error;92 return { messageId: data?.id };93 },94});95```96~97Τρία πράγματα να σημειώσετε:98~991. **Κανένα timeout στο σώμα run.** Η πλατφόρμα διαχειρίζεται τον χρόνο εκτέλεσης μέσω `maxDuration` στο config, όχι στο runtime.1002. **Τα throws είναι retries.** Το SDK πιάνει εξαιρέσεις και τρέχει ξανά με exponential backoff σύμφωνα με την πολιτική `retry`.1013. **Η τιμή επιστροφής διατηρείται.** Άλλα tasks και το frontend σας μπορούν να διαβάζουν `run.output` από οπουδήποτε.102~103## Triggering Tasks104~105Καλείτε ένα task από το backend σας, τις διαδρομές API ή ένα άλλο task.106~107```typescript108import { sendWelcomeEmail } from "@/trigger/send-welcome-email";109~110const handle = await sendWelcomeEmail.trigger(111 { email: "user@example.com", name: "Alex" },112 {113 idempotencyKey: `welcome-${userId}`,114 concurrencyKey: `tenant-${tenantId}`,115 queue: { name: "emails", concurrencyLimit: 50 },116 delay: "30s",117 ttl: "10m",118 }119);120~121console.log(handle.id); // run_xyz - χρησιμοποιήστε το για παρακολούθηση ή εμφάνιση προόδου122```123~124Οι επιλογές ξεκλειδώνουν πολλή συμπεριφορά σε μία κλήση:125~126- **`idempotencyKey`** - αν ένα run με το ίδιο κλειδί υπάρχει ήδη, το SDK επιστρέφει το υπάρχον handle αντί να διπλασιάσει την εργασία.127- **`concurrencyKey`** - σειριοποιεί τα runs που μοιράζονται το κλειδί ώστε να μην ξεπερνάτε ένα per-tenant rate limit.128- **`queue.concurrencyLimit`** - παγκόσμιο cap για την ουρά σε όλα τα κλειδιά.129- **`delay`** - προγραμματίζει το run για μελλοντικό χρόνο.130- **`ttl`** - αν το run δεν έχει ξεκινήσει μέχρι τότε, λήγει αυτόματα.131~132### Batch trigger133~134Για fan-out workloads, το `batchTrigger` δέχεται έως 500 items ανά κλήση και δημιουργεί ένα run ανά item.135~136```typescript137await sendWelcomeEmail.batchTrigger(138 newUsers.map((u) => ({139 payload: { email: u.email, name: u.name },140 options: { idempotencyKey: `welcome-${u.id}` },141 }))142);143```144~145## Προγραμματισμένα Tasks146~147Τα cron jobs γίνονται δηλώσεις πρώτης τάξης. Το ίδιο το schedule είναι ένα ξεχωριστό αντικείμενο που μπορείτε να επισυνάψετε σε ένα task πολλές φορές.148~149```typescript150// trigger/daily-digest.ts151import { schedules } from "@trigger.dev/sdk";152~153export const dailyDigest = schedules.task({154 id: "daily-digest",155 cron: "0 9 * * *",156 run: async (payload) => {157 console.log("Scheduled at:", payload.timestamp);158 console.log("Last run:", payload.lastTimestamp);159 console.log("Timezone:", payload.timezone);160 console.log("Next 5 runs:", payload.upcoming);161~162 await sendDigestForDate(payload.timestamp);163 },164});165```166~167Για schedules per-tenant - ας πούμε, ένα cron ανά πελάτη - τα δημιουργείτε δυναμικά μέσω του management API.168~169```typescript170import { schedules } from "@trigger.dev/sdk";171~172await schedules.create({173 task: "daily-digest",174 cron: "0 9 * * *",175 timezone: "America/New_York",176 externalId: `customer_${customerId}`,177 deduplicationKey: `digest-${customerId}`,178});179```180~181Το `deduplicationKey` καθιστά την κλήση idempotent: η εκ νέου εκτέλεση του ίδιου κώδικα κατά την ώρα του deploy δεν στοιβάζει διπλασιασμένα schedules.182~183## Ουρές, ταυτόχρονη εκτέλεση και idempotency184~185Τρία primitives καλύπτουν τις περισσότερες ανάγκες rate-limiting και ταξινόμησης.186~187```mermaid188graph TB189 Trigger[trigger payload] --> IK{idempotencyKey<br/>είδες;}190 IK -->|ναι| Reuse[Επιστροφή υπάρχοντος run]191 IK -->|όχι| CK[bucket concurrencyKey]192 CK --> Q[Ουρά με<br/>concurrencyLimit]193 Q -->|slot διαθέσιμο| Run[Εκτέλεση task]194 Q -->|slots γεμάτα| Wait[Αναμονή στην ουρά]195```196~197Ένα κοινό μοτίβο: μία ουρά ανά tenant με μικρή per-key ταυτόχρονη εκτέλεση για να σεβαστείτε το rate limit ενός vendor, συν ένα idempotency key για να κάνετε τα retries ασφαλή.198~199```typescript200await syncShopifyOrders.trigger(201 { shopId },202 {203 queue: { name: `shopify-${shopId}`, concurrencyLimit: 2 },204 concurrencyKey: shopId,205 idempotencyKey: `sync-${shopId}-${Date.now() / 60_000 | 0}`,206 }207);208```209~210## Αναμονές και εργασία μεγάλης διάρκειας211~212Τα tasks μπορούν να σταματήσουν χωρίς να κρατούν σύνδεση ή να καίνε compute. Η πλατφόρμα διατηρεί την κατάσταση και επαναφέρει τη συνάρτηση όταν η αναμονή ολοκληρωθεί.213~214```typescript215import { wait } from "@trigger.dev/sdk";216~217export const onboarding = task({218 id: "onboarding",219 run: async (payload: { userId: string }) => {220 await sendWelcomeEmail.triggerAndWait({ userId: payload.userId });221 await wait.for({ days: 1 });222 await sendTipsEmail.trigger({ userId: payload.userId });223 await wait.until({ date: oneWeekFromSignup(payload.userId) });224 await sendUpgradeOffer.trigger({ userId: payload.userId });225 },226});227```228~229Το `triggerAndWait` είναι το killer feature: ενεργοποιεί ένα child task και αναστέλλει το parent μέχρι να ολοκληρωθεί το child. Συνθέτετε tasks σαν async συναρτήσεις, αλλά η ενορχήστρωση τρέχει ανθεκτικά για ημέρες ή εβδομάδες.230~231### Human-in-the-loop με `wait.forToken`232~233Για ροές έγκρισης και AI gates, το `wait.forToken` σταματάει μέχρι η εφαρμογή σας να καλέσει πίσω με ένα αποτέλεσμα.234~235```typescript236import { task, wait } from "@trigger.dev/sdk";237~238export const publishPost = task({239 id: "publish-post",240 run: async (payload: { draftId: string }) => {241 const draft = await generateAIContent(payload.draftId);242~243 const token = await wait.createToken({ timeout: "7d" });244 await notifyEditor({ draftId: draft.id, token: token.id });245~246 const decision = await wait.forToken<{ approved: boolean; notes?: string }>(247 token.id248 );249~250 if (decision.approved) {251 return await publish(draft);252 }253 return await applyFeedback(draft, decision.notes);254 },255});256```257~258Ο επεξεργαστής ανοίγει ένα UI, αναθεωρεί το πρόχειρο, κάνει κλικ στο Approve και το backend σας ολοκληρώνει το token. Το task συνεχίζει από εκεί που σταμάτησε - ακόμη και αν έχουν περάσει ώρες ή ημέρες.259~260## Lifecycle Hooks261~262Μπορείτε να επισυνάψετε `init`, `onStart`, `onSuccess` και `onFailure` σε ένα task ή παγκοσμίως στο `trigger.config.ts`. Χρησιμοποιήστε τα για tracing, error reporting και κοινόχρηστο setup.263~264```typescript265// trigger.config.ts266export default defineConfig({267 // ...268 init: async () => {269 Sentry.init({ dsn: process.env.SENTRY_DSN });270 },271 onFailure: async ({ error, ctx }) => {272 Sentry.captureException(error, {273 tags: { taskId: ctx.task.id, runId: ctx.run.id },274 });275 },276});277```278~279Το `init` τρέχει μία φορά ανά worker container κατά την εκκίνηση, όχι ανά run, οπότε είναι το σωστό μέρος για να ρυθμίσετε clients και pools.280~281## Realtime στο Frontend282~283Το Trigger.dev δημοσιεύει αλλαγές κατάστασης run - status, metadata, output - μέσω ενός streaming API. Τα React hooks εγγράφονται σε αυτό το stream και κάνουν re-render αυτόματα.284~285```typescript286// trigger/process-video.ts287import { task, metadata } from "@trigger.dev/sdk";288~289export const processVideo = task({290 id: "process-video",291 run: async (payload: { videoId: string }) => {292 metadata.set("stage", "transcoding");293 await transcode(payload.videoId);294~295 metadata.set("stage", "thumbnails");296 await generateThumbnails(payload.videoId);297~298 metadata.set("stage", "uploading");299 const url = await uploadToCDN(payload.videoId);300~301 return { url };302 },303});304```305~306```tsx307// components/VideoStatus.tsx308"use client";309import { useRealtimeRun } from "@trigger.dev/react-hooks";310import type { processVideo } from "@/trigger/process-video";311~312export function VideoStatus({313 runId,314 publicAccessToken,315}: {316 runId: string;317 publicAccessToken: string;318}) {319 const { run, error } = useRealtimeRun<typeof processVideo>(runId, {320 accessToken: publicAccessToken,321 });322~323 if (error) return <p>Error: {error.message}</p>;324 if (!run) return <p>Loading...</p>;325~326 return (327 <div>328 <p>Status: {run.status}</p>329 <p>Stage: {String(run.metadata?.stage ?? "queued")}</p>330 {run.output?.url && <video src={run.output.url} controls />}331 </div>332 );333}334```335~336Δημιουργείτε το public access token στην πλευρά του server, scoped σε ένα συγκεκριμένο run, και το στέλνετε στον client. Το hook χειρίζεται το auth, την επανασύνδεση και τις σταδιακές ενημερώσεις.337~338Για trigger-and-subscribe σε ένα βήμα:339~340```tsx341import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks";342~343const { submit, run, isLoading } = useRealtimeTaskTrigger<typeof processVideo>(344 "process-video",345 { accessToken: publicAccessToken }346);347~348<button onClick={() => submit({ videoId })} disabled={isLoading}>349 Process video350</button>;351```352~353## AI Agents και Streaming354~355Το Trigger.dev έχει γίνει ένα δημοφιλές runtime για AI agents επειδή τα ίδια primitives - ανθεκτική εκτέλεση, retries, αναμονές, real-time metadata, human-in-the-loop - είναι ακριβώς αυτό που χρειάζονται οι agents. Streamάρετε tokens από έναν model provider στο `metadata` ενώ το run εκτελείται, το frontend τα κάνει render ζωντανά και το run επιβιώνει από tool calls μεγάλης διάρκειας χωρίς να καίει ένα serverless timeout.356~357```typescript358import { task, metadata } from "@trigger.dev/sdk";359import { streamText } from "ai";360import { anthropic } from "@ai-sdk/anthropic";361~362export const researchAgent = task({363 id: "research-agent",364 maxDuration: 1800,365 run: async (payload: { question: string }) => {366 const result = streamText({367 model: anthropic("claude-opus-4-7"),368 system: "You are a research assistant. Use the web.",369 prompt: payload.question,370 tools: { webSearch },371 });372~373 let fullText = "";374 for await (const chunk of result.textStream) {375 fullText += chunk;376 metadata.set("partial", fullText);377 }378~379 return { answer: fullText, usage: await result.usage };380 },381});382```383~384Το frontend χρησιμοποιεί το `useRealtimeRun` και διαβάζει το `run.metadata.partial` για να κάνει render την streaming απάντηση, με τον ίδιο τρόπο που θα κάνατε render μια chat completion - εκτός από το ότι αυτή επιβιώνει σε πλήρες reload σελίδας.385~386## Deploying387~388Τα deploys μεταγλωττίζουν τα tasks σας σε ένα versioned bundle, χτίζουν ένα container και ανταλλάσσουν την κίνηση ατομικά. Τα παλιά in-flight runs συνεχίζουν να χρησιμοποιούν την προηγούμενη έκδοση.389~390```bash391npx trigger.dev@latest deploy --env prod392```393~394Στο CI συνήθως συνδέετε αυτό με το ίδιο workflow που στέλνει την εφαρμογή σας:395~396```yaml397# .github/workflows/deploy.yml398- name: Deploy Trigger.dev399 env:400 TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}401 run: npx trigger.dev@latest deploy --env prod402```403~404Για περιβάλλοντα preview, περάστε `--env preview --branch ${{ github.head_ref }}` και το Trigger.dev δημιουργεί ένα απομονωμένο περιβάλλον ανά branch, αντικατοπτρίζοντας τον τρόπο με τον οποίο το Vercel χειρίζεται τα preview deployments.405~406## Self-Hosting vs Cloud407~408Το Trigger.dev είναι ανοικτού κώδικα υπό την άδεια Apache 2.0. Μπορείτε να κάνετε self-host σε οποιαδήποτε πλατφόρμα container (Docker Compose, Kubernetes, Fly.io) ή να χρησιμοποιήσετε το διαχειριζόμενο cloud στο trigger.dev.409~410| Πτυχή | Cloud | Self-hosted |411|--------|-------|-------------|412| **Setup** | Εγγραφή, εκτέλεση `init` | Εκτέλεση docker-compose ή Helm chart |413| **Scaling** | Αυτόματο | Δική σας ευθύνη |414| **Pricing** | Ανά run + ανά compute | Μόνο κόστος infra |415| **Compliance** | SOC 2 | Ό,τι παρέχει το περιβάλλον σας |416| **Καλύτερο για** | Τις περισσότερες ομάδες | Αυστηρή κατοικία δεδομένων, custom infra |417~418Το SDK και το CLI είναι πανομοιότυπα μεταξύ των τρόπων - αλλάζετε ένα profile flag και δείχνετε στο δικό σας instance.419~420## Best Practices421~422### 1. Κρατήστε τα payloads μικρά και σειριοποιήσιμα423~424Περάστε IDs και αναφορές, όχι πλήρη αντικείμενα. Τραβήξτε τα δεδομένα μέσα στο task. Αυτό κρατάει την ουρά μικρή, τα payloads φθηνά να καταγραφούν και σας επιτρέπει να αλλάξετε την πηγή δεδομένων χωρίς re-trigger.425~426### 2. Idempotency keys σε κάθε εξωτερική κλήση427~428Συνδυάστε το `idempotencyKey` στο trigger του task με idempotency keys στα APIs των vendors σας (Stripe, OpenAI, κλπ). Τα retries θα είναι ασφαλή end-to-end.429~430### 3. Χρησιμοποιήστε το `triggerAndWait` για ενορχήστρωση, όχι `Promise.all` από triggers431~432Ένας parent που καλεί `triggerAndWait` συνθέτει ανθεκτικά child tasks. Ένας parent που τρενάρει και επιλύει αμέσως χάνει την παρατηρησιμότητα της αλυσίδας.433~434### 4. Tagάρετε τα runs435~436Προσθέστε `tags` στα triggers (`tags: ["user:123", "feature:onboarding"]`) ώστε να μπορείτε να φιλτράρετε το dashboard και το management API ανά επιχειρηματικές διαστάσεις.437~438### 5. Κρατήστε το `init` idempotent439~440Τρέχει σε κάθε cold start. Αποφύγετε migrations ή one-shot παρενέργειες εκεί.441~442## Συμπέρασμα443~444Το Trigger.dev αφαιρεί τις κατηγορίες εργασίας που παλιότερα απαιτούσαν να χτίσετε ένα job system από την αρχή. Γράφετε async TypeScript, το καλείτε από οπουδήποτε και η πλατφόρμα σας δίνει ανθεκτική εκτέλεση, scheduling, ουρές, retries, real-time ενημερώσεις και πρότυπα human-in-the-loop out of the box.445~446Η ίδια επιφάνεια που τροφοδοτεί ένα νυχτερινό cron είναι η επιφάνεια που τροφοδοτεί έναν multi-step AI agent που streamάρει στο frontend και σταματάει για review. Αυτή η σύγκλιση είναι αυτό που κάνει το framework άξιο σοβαρής ματιάς το 2026, είτε διαχειρίζεστε ένα SaaS που χρειάζεται αξιόπιστη εργασία παρασκηνίου είτε αποστέλλετε χαρακτηριστικά AI που επιβιώνουν από ένα serverless timeout.447~448> **Checklist Εκκίνησης:**449>450> - [x] Εγγραφείτε στο trigger.dev ή τρέξτε το self-hosted Docker stack451> - [x] `npx trigger.dev@latest init` στο έργο σας452> - [x] Ορίστε το πρώτο σας task με `task({ id, run })`453> - [x] Triggerάρετέ το από το API σας και δείτε το run στο dashboard454> - [x] Προσθέστε `idempotencyKey` και `concurrencyKey` για ασφάλεια production455> - [x] Συνδέστε το `useRealtimeRun` με ένα status component456> - [x] Κάντε deploy με `trigger.dev deploy --env prod` από το CI457~
NORMAL · trigger-dev-background-jobs-guide.md [readonly]457 lines · :q to close