Compare commits

...

31 Commits

Author SHA1 Message Date
Eric Allam 59277731c4 Fix pnpm lock file 2023-11-16 16:32:14 +00:00
github-actions[bot] 742d16087b chore: Update version for release (#739)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-16 16:31:41 +00:00
Eric Allam 0ab51d62bc Don’t send notifications for internal runs and add isTest to the notification
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 19s
🚀 Publish Trigger.dev Docker / units (push) Failing after 3s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 6s
2023-11-16 14:45:39 +00:00
Eric Allam 756024da78 Add support for Run Notifications #401 (#738)
* Add support for Run Notifications #401

* Fixed typo and added changeset
2023-11-16 14:19:42 +00:00
Dan c47ad5e038 Docs improvements and added API catalog links (#737)
* integration docs various improvements

* Added optional comments to step by step guide

* Added API catalog card to the integrations intro

* Added Api catalog button to the examples job tab

* Tweaked copy of api catalog card

* Update introduction.mdx

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-11-16 11:50:26 +00:00
Dan 888344797e Added code examples and modified blank state on the integrations & APIs page (#736)
* Added more API icons

* Updated the title

* Added resources.codeexample.ts to pull in example code from github

* Added regex to remove redundant express code from the examples

* Added cal examples

* Code changes onSubmit

* Improved the code loading state

* Removed request integration button

* Improved the Customhelp copy

* Fixed styling issues and removed imports

* Added all the code examples

* Don’t show title if there’s 1 or less examples

* Updated copy for the non example version

* Added example code to the apis without examples, added a focus state

* Fixed titles

* Got rid of the phantom gap

* Made the copy more generic so it works on the integration pages as well

* Removed import

* Added view examples button to the integration pages

* Removed fetch/sdk option from integration sheet

* Apply code example regex on the server

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-11-15 17:38:24 +00:00
Eric Allam 4a7caa68b6 Record when run executions start and finish
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-11-15 15:18:32 +00:00
Matt Aitken 3bc54d36f7 SQS logging: fixed error and message, plus using tracing 2023-11-15 13:32:40 +00:00
Matt Aitken 5295a3f798 Changed SQS error callbacks to log to error 2023-11-15 13:26:57 +00:00
Eric Allam 14429b8b66 Fix “transaction already closed” error when running tasks (#735) 2023-11-14 23:22:34 +00:00
Matt Aitken 5dd1bcc589 sendEvents proxy uses batches of 10 (max for SQS) 2023-11-14 21:34:08 +00:00
Matt Aitken ed5ee16de5 Better errors now get to users of sendEvent 2023-11-14 21:33:47 +00:00
Matt Aitken 076185d19f Added a perf test for sendEvents 2023-11-14 21:32:46 +00:00
Matt Aitken 1e07964d81 The parallel perf test is now driven by numbers 2023-11-14 15:46:34 +00:00
Matt Aitken f406e59c45 Configuration and docs for deploying the proxy to Cloudflare 2023-11-14 14:04:42 +00:00
Eric Allam 75550f535c Fix undefined task outputs
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 19s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
And also fix the SSE connection getting reset on a infinite loop when runs are complete
2023-11-13 16:18:41 +00:00
Matt Aitken 2b48e6d04b Feature: sendEvent can use an AWS SQS queue to increase reliability and throughput (#729)
* Initial commit (by create-cloudflare CLI)

* Changed the prettier rules

* Started writing Readme and got dev working

* Validate the request, parse the event and respond with the correct format

* Use ulidx instead of ulid in core

* Removed some boilerplate and use new function for sendEvent

* Removed old ulid package

* When handling a webhook, it’s not an error if the HTTP endpoint environment isn’t found

* If the env vars aren’t set, log the request and pass it through

* Pass requests through. Setup the env vars

* Flipped the API key detection because we can enforce that it’s the shape of a private API key

* Progress with sendEvent proxy

* Events are being put on the SQS queue

* The SQS event queue is working

* Make sure there’s a timestamp before enqueuing the evnet

* Log the sqs_event, not API key

* Added a bit more detail to the readme

* Improved the Cloudflare logs

* We don’t need the global.window hack for AWS, just globalThis

* Removed commented out wrangler.toml values

* Reworked the proxy to make it easier to add more endpoints

* Use json utility to send Responses

* Start work on proxying bulk events
2023-11-13 15:08:01 +00:00
Matt Aitken fe14947bb0 Removed waitForEvent beta warning. Made a snippet so we can use this in future easily. 2023-11-10 18:24:48 +00:00
Matt Aitken 0afa119721 Use node-fetch for invokeEphemeralEvents instead of @whatwg-node/fetch (#734)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-11-10 17:36:21 +00:00
Eric Allam af57bc208a Update pnpm lock file 2023-11-10 16:54:06 +00:00
Eric Allam 7cbbb26038 Merge pull request #730 from triggerdotdev/changeset-release/main
chore: Update version for release
2023-11-10 16:49:47 +00:00
github-actions[bot] 2e5f8d8de3 chore: Update version for release 2023-11-10 16:05:39 +00:00
Eric Allam 9a7c08c26a Improvements: Fix dangling SSE issue and compression memory leak (#733)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 1s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Downgrade to remix-auth-email-link to remove yarn dependency

* Turn off the pg listen service for now

* Add snapshot admin route

* A couple logger fixes

* Add ability to disable compression

* Add ability to disable SSE

* Fix SSE memory leak + DB load issue
2023-11-10 16:02:53 +00:00
Eric Allam e8e7c116d1 Remove the pg listen code to see if it’s causing DB issues 2023-11-09 21:52:48 +00:00
Matt Aitken 99dd6673f9 Test page display a message if there are no environments that the logged in user can run tests with 2023-11-09 20:12:20 +00:00
Matt Aitken a41d9b3e67 Fix for first endpoint sheet React error 2023-11-09 20:11:36 +00:00
Eric Allam cb1825bfaf Add OpenAI support for 4.16.0 (#726)
* Add OpenAI support for 4.16.0

* Add support for background polling and use that in OpenAI integration to power assistants

* Much improved OpenAI docs

* Added backgroundPoll docs

* Implements waitForEvent and added docs for more built in tasks

* Add sendEvent API referenc

* Write the task libray

* Add changeset and warning for waitForEvent
2023-11-09 16:59:58 +00:00
nicktrn d02173442c Feature: io.sendEvents() (#728)
* Add io.sendEvents

* Add examples to built-ins catalog entry

* Add docs

* Add changeset

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-11-09 16:42:23 +00:00
Matt Aitken 9f1f59cc81 Added the redirect to the invites page back in… 2023-11-09 16:30:21 +00:00
Matt Aitken e48c9b5e69 Made it clearer in the HTTP endpoints docs that we’re using Cal.com as an example 2023-11-08 10:15:42 +00:00
Eric Allam 55a9b96c88 Remove the coming soon warning 2023-11-07 16:31:44 +00:00
225 changed files with 10542 additions and 1903 deletions
+7
View File
@@ -0,0 +1,7 @@
REWRITE_HOSTNAME=
AWS_SQS_ACCESS_KEY_ID=
AWS_SQS_SECRET_ACCESS_KEY=
AWS_SQS_QUEUE_URL=
AWS_SQS_REGION=
#optional
#REWRITE_PORT=
+13
View File
@@ -0,0 +1,13 @@
# http://editorconfig.org
root = true
[*]
indent_style = tab
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
+172
View File
@@ -0,0 +1,172 @@
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# wrangler project
.dev.vars
.wrangler/
+11
View File
@@ -0,0 +1,11 @@
{
"semi": true,
"singleQuote": false,
"jsxSingleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}
+8
View File
@@ -0,0 +1,8 @@
# proxy
## 0.0.1
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/core@2.2.7
+68
View File
@@ -0,0 +1,68 @@
# Trigger.dev proxy
This is an optional module that can be used to proxy and queue requests to the Trigger.dev API.
## Why?
The Trigger.dev API is designed to be fast and reliable. However, if you have a lot of traffic, you may want to use this proxy to queue requests to the API. It intercepts some requests to the API and adds them to an AWS SQS queue, then the webapp can be setup to process the queue.
## Current features
- Intercepts `sendEvent` requests and adds them to an AWS SQS queue. The webapp then reads from the queue and creates the events.
## Setup
### Create an AWS SQS queue
In AWS you should create a new AWS SQS queue with appropriate security settings. You will need the queue URL for the next step.
### Environment variables
#### Cloudflare secrets
Locally you should copy the `.dev.var.example` file to `.dev.var` and fill in the values.
When deploying you should use `wrangler` (the Cloudflare CLI tool) to set secrets. Make sure you set the correct --env ("staging" or "prod")
```bash
wrangler secret put REWRITE_HOSTNAME --env staging
wrangler secret put AWS_SQS_ACCESS_KEY_ID --env staging
wrangler secret put AWS_SQS_SECRET_ACCESS_KEY --env staging
wrangler secret put AWS_SQS_QUEUE_URL --env staging
wrangler secret put AWS_SQS_REGION --env staging
```
You need to set your API CNAME entry to be proxied by Cloudflare. You can do this in the Cloudflare dashboard.
#### Webapp
These env vars also need setting in the webapp.
```bash
AWS_SQS_REGION
AWS_SQS_ACCESS_KEY_ID
AWS_SQS_SECRET_ACCESS_KEY
AWS_SQS_QUEUE_URL
AWS_SQS_BATCH_SIZE
```
## Deployment
Staging:
```bash
npx wrangler@latest deploy --route "<your-api-subdomain>/*" --env staging
```
Prod:
```bash
npx wrangler@latest deploy --route "<your-api-subdomain>/*" --env prod
```
## Development
Set the environment variables as described above.
1. `pnpm install`
2. `pnpm run dev --filter proxy`
+21
View File
@@ -0,0 +1,21 @@
{
"name": "proxy",
"version": "0.0.1",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230419.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
},
"dependencies": {
"@aws-sdk/client-sqs": "^3.445.0",
"@trigger.dev/core": "workspace:*",
"ulidx": "^2.2.1",
"zod": "3.22.3",
"zod-error": "1.5.0"
}
}
+20
View File
@@ -0,0 +1,20 @@
import { z } from "zod";
const AuthorizationHeaderSchema = z.string().regex(/^Bearer .+$/);
export function getApiKeyFromRequest(request: Request) {
const rawAuthorization = request.headers.get("Authorization");
const authorization = AuthorizationHeaderSchema.safeParse(rawAuthorization);
if (!authorization.success) {
return;
}
const apiKey = authorization.data.replace(/^Bearer /, "");
const type = isPrivateApiKey(apiKey) ? ("PRIVATE" as const) : ("PUBLIC" as const);
return { apiKey, type };
}
function isPrivateApiKey(key: string) {
return key.startsWith("tr_");
}
+87
View File
@@ -0,0 +1,87 @@
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
import { ApiEventLog, SendEventBodySchema } from "@trigger.dev/core";
import { generateErrorMessage } from "zod-error";
import { Env } from "..";
import { getApiKeyFromRequest } from "../apikey";
import { json } from "../json";
import { calculateDeliverAt } from "./utils";
/** Adds the event to an AWS SQS queue, so it can be consumed from the main Trigger.dev API */
export async function queueEvent(request: Request, env: Env): Promise<Response> {
//check there's a private API key
const apiKeyResult = getApiKeyFromRequest(request);
if (!apiKeyResult || apiKeyResult.type !== "PRIVATE") {
return json(
{ error: "Invalid or Missing API key" },
{
status: 401,
}
);
}
//parse the request body
try {
const anyBody = await request.json();
const body = SendEventBodySchema.safeParse(anyBody);
if (!body.success) {
return json(
{ error: generateErrorMessage(body.error.issues) },
{
status: 422,
}
);
}
// The AWS SDK tries to use crypto from off of the window,
// so we need to trick it into finding it where it expects it
globalThis.global = globalThis;
const client = new SQSClient({
region: env.AWS_SQS_REGION,
credentials: {
accessKeyId: env.AWS_SQS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY,
},
});
const timestamp = body.data.event.timestamp ?? new Date();
//add the event to the queue
const send = new SendMessageCommand({
// use wrangler secrets to provide this global variable
QueueUrl: env.AWS_SQS_QUEUE_URL,
MessageBody: JSON.stringify({
event: { ...body.data.event, timestamp },
options: body.data.options,
apiKey: apiKeyResult.apiKey,
}),
});
const queuedEvent = await client.send(send);
console.log("Queued event", queuedEvent);
//respond with the event
const event: ApiEventLog = {
id: body.data.event.id,
name: body.data.event.name,
payload: body.data.event.payload,
context: body.data.event.context,
timestamp,
deliverAt: calculateDeliverAt(body.data.options),
};
return json(event, {
status: 200,
});
} catch (e) {
console.error("queueEvent error", e);
return json(
{
error: `Failed to send event: ${e instanceof Error ? e.message : JSON.stringify(e)}`,
},
{
status: 422,
}
);
}
}
+112
View File
@@ -0,0 +1,112 @@
import { SQSClient, SendMessageBatchCommand } from "@aws-sdk/client-sqs";
import { ApiEventLog, SendBulkEventsBodySchema } from "@trigger.dev/core";
import { generateErrorMessage } from "zod-error";
import { Env } from "..";
import { getApiKeyFromRequest } from "../apikey";
import { json } from "../json";
import { calculateDeliverAt } from "./utils";
/** Adds the event to an AWS SQS queue, so it can be consumed from the main Trigger.dev API */
export async function queueEvents(request: Request, env: Env): Promise<Response> {
//check there's a private API key
const apiKeyResult = getApiKeyFromRequest(request);
if (!apiKeyResult || apiKeyResult.type !== "PRIVATE") {
return json(
{ error: "Invalid or Missing API key" },
{
status: 401,
}
);
}
//parse the request body
try {
const anyBody = await request.json();
const body = SendBulkEventsBodySchema.safeParse(anyBody);
if (!body.success) {
return json(
{ error: generateErrorMessage(body.error.issues) },
{
status: 422,
}
);
}
// The AWS SDK tries to use crypto from off of the window,
// so we need to trick it into finding it where it expects it
globalThis.global = globalThis;
const client = new SQSClient({
region: env.AWS_SQS_REGION,
credentials: {
accessKeyId: env.AWS_SQS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY,
},
});
const updatedEvents: ApiEventLog[] = body.data.events.map((event) => {
const timestamp = event.timestamp ?? new Date();
return {
...event,
payload: event.payload,
timestamp,
};
});
//divide updatedEvents into multiple batches of 10 (max size SQS accepts)
const batches: ApiEventLog[][] = [];
let currentBatch: ApiEventLog[] = [];
for (let i = 0; i < updatedEvents.length; i++) {
currentBatch.push(updatedEvents[i]);
if (currentBatch.length === 10) {
batches.push(currentBatch);
currentBatch = [];
}
}
if (currentBatch.length > 0) {
batches.push(currentBatch);
}
//loop through the batches and send them
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
//add the event to the queue
const send = new SendMessageBatchCommand({
// use wrangler secrets to provide this global variable
QueueUrl: env.AWS_SQS_QUEUE_URL,
Entries: batch.map((event, index) => ({
Id: `event-${index}`,
MessageBody: JSON.stringify({
event,
options: body.data.options,
apiKey: apiKeyResult.apiKey,
}),
})),
});
const queuedEvent = await client.send(send);
console.log("Queued events", queuedEvent);
}
//respond with the events
const events: ApiEventLog[] = updatedEvents.map((event) => ({
...event,
payload: event.payload,
deliverAt: calculateDeliverAt(body.data.options),
}));
return json(events, {
status: 200,
});
} catch (e) {
console.error("queueEvents error", e);
return json(
{
error: `Failed to send events: ${e instanceof Error ? e.message : JSON.stringify(e)}`,
},
{
status: 422,
}
);
}
}
+15
View File
@@ -0,0 +1,15 @@
import { SendEventOptions } from "@trigger.dev/core";
export function calculateDeliverAt(options?: SendEventOptions) {
// If deliverAt is a string and a valid date, convert it to a Date object
if (options?.deliverAt) {
return options?.deliverAt;
}
// deliverAfter is the number of seconds to wait before delivering the event
if (options?.deliverAfter) {
return new Date(Date.now() + options.deliverAfter * 1000);
}
return undefined;
}
+67
View File
@@ -0,0 +1,67 @@
import { queueEvent } from "./events/queueEvent";
import { queueEvents } from "./events/queueEvents";
export interface Env {
/** The hostname needs to be changed to allow requests to pass to the Trigger.dev platform */
REWRITE_HOSTNAME: string;
REWRITE_PORT?: string;
AWS_SQS_ACCESS_KEY_ID: string;
AWS_SQS_SECRET_ACCESS_KEY: string;
AWS_SQS_QUEUE_URL: string;
AWS_SQS_REGION: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (!env.REWRITE_HOSTNAME) throw new Error("Missing REWRITE_HOSTNAME");
console.log("url", request.url);
if (!queueingIsEnabled(env)) {
console.log("Missing AWS credentials. Passing through to the origin.");
return redirectToOrigin(request, env);
}
const url = new URL(request.url);
switch (url.pathname) {
case "/api/v1/events": {
if (request.method === "POST") {
return queueEvent(request, env);
}
break;
}
case "/api/v1/events/bulk": {
if (request.method === "POST") {
return queueEvents(request, env);
}
break;
}
}
//the same request but with the hostname (and port) changed
return redirectToOrigin(request, env);
},
};
function redirectToOrigin(request: Request, env: Env) {
const newUrl = new URL(request.url);
newUrl.hostname = env.REWRITE_HOSTNAME;
newUrl.port = env.REWRITE_PORT || newUrl.port;
const requestInit: RequestInit = {
method: request.method,
headers: request.headers,
body: request.body,
};
console.log("rewritten url", newUrl.toString());
return fetch(newUrl.toString(), requestInit);
}
function queueingIsEnabled(env: Env) {
return (
env.AWS_SQS_ACCESS_KEY_ID &&
env.AWS_SQS_SECRET_ACCESS_KEY &&
env.AWS_SQS_QUEUE_URL &&
env.AWS_SQS_REGION
);
}
+13
View File
@@ -0,0 +1,13 @@
export function json(body: any, init?: ResponseInit) {
const headers = {
"content-type": "application/json",
...(init?.headers ?? {}),
};
const responseInit: ResponseInit = {
...(init ?? {}),
headers,
};
return new Response(JSON.stringify(body), responseInit);
}
+35
View File
@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react" /* Specify what JSX code is generated. */,
"module": "es2022" /* Specify what module code is generated. */,
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"types": [
"@cloudflare/workers-types"
] /* Specify type package names to be included without being referenced in a source file. */,
"resolveJsonModule": true /* Enable importing .json files */,
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
"noEmit": true /* Disable emitting files from a compilation. */,
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"baseUrl": ".",
"paths": {
"@trigger.dev/core": ["../../packages/core/src/index"],
"@trigger.dev/core/*": ["../../packages/core/src/*"]
}
}
}
+7
View File
@@ -0,0 +1,7 @@
name = "proxy"
main = "src/index.ts"
compatibility_date = "2023-10-30"
compatibility_flags = [ "nodejs_compat" ]
[env.staging]
[env.prod]
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { ApiAuthenticationMethodApiKey, Integration } from "~/services/externalApis/types";
import { docsIntegrationPath } from "~/utils/pathBuilder";
import { Integration } from "~/services/externalApis/types";
import { apiReferencePath, docsIntegrationPath } from "~/utils/pathBuilder";
import { LinkButton } from "../primitives/Buttons";
import { Header1, Header2 } from "../primitives/Headers";
import { NamedIconInBox } from "../primitives/NamedIcon";
@@ -48,6 +48,15 @@ export function ConnectToIntegrationSheet({
<Paragraph variant="small">{integration.description}</Paragraph>
)}
</div>
<LinkButton
to={apiReferencePath(integration.identifier)}
variant="secondary/small"
TrailingIcon="arrow-up-right"
trailingIconClassName="h-4 w-4 text-slate-400"
target="_blank"
>
View examples
</LinkButton>
<LinkButton
to={docsIntegrationPath(integration.identifier)}
variant="secondary/small"
@@ -83,13 +92,6 @@ export function ConnectToIntegrationSheet({
variant="description"
/>
)}
<RadioGroupItem
id="custom"
value="custom"
label="Fetch/Existing SDK"
description={`Alternatively, use ${integration.name} without our integration.`}
variant="description"
/>
</RadioGroup>
{integrationMethod && (
@@ -132,7 +134,5 @@ function SelectedIntegrationMethod({
callbackUrl={callbackUrl}
/>
);
case "custom":
return <CustomHelp name={integration.name} />;
}
}
@@ -1,79 +1,124 @@
import { CodeBlock } from "../code/CodeBlock";
import { useState } from "react";
import { CodeExample } from "~/routes/resources.codeexample";
import { Api } from "~/services/externalApis/apis.server";
import { cn } from "~/utils/cn";
import { Feedback } from "../Feedback";
import { Header1, Header2 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
import { TextLink } from "../primitives/TextLink";
const fallbackExamples = [
{
title: "Post to Slack when meetings are booked or cancelled.",
slug: "cal-slack-meeting-alert",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/cal-http-endpoint.ts",
},
{
title: "Translate some text with DeepL.",
slug: "translate-text-with-deepl",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/deepl.ts",
},
{
title: "Create a Discord bot and send a message to a channel.",
slug: "discord-bot-send-message",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/discord.ts",
},
{
title: "Retrieve a Notion page by ID.",
slug: "retrieve-notion-page",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/notion.ts",
},
];
export function CustomHelp({ api }: { api: Api }) {
const [selectedExample, setSelectedExample] = useState(0);
const changeCodeExample = (index: number) => {
setSelectedExample(index);
};
export function CustomHelp({ name }: { name: string }) {
return (
<div className="mt-4">
<Header1 className="mb-2">You can use any API with requests or an SDK</Header1>
<Header2 className="mb-2">How to use an SDK</Header2>
<Header1 className="mb-2">Using an API with an SDK or requests</Header1>
<Paragraph spacing>
You can call SDK methods from inside the run function, but you should wrap them in a Task to
make sure they're resumable.
You can use Trigger.dev with any existing Node SDK or even just using fetch. You can
subscribe to any API with{" "}
<TextLink href="https://trigger.dev/docs/documentation/concepts/http-endpoints">
HTTP endpoints
</TextLink>{" "}
and perform actions by wrapping tasks using{" "}
<TextLink
href="https://trigger.dev/docs/documentation/guides/writing-jobs-step-by-step#create-your-own-tasks"
className="font-mono"
>
io.runTask
</TextLink>
. This makes your background job resumable and appear in our dashboard.
</Paragraph>
<Paragraph spacing>Here's an example with the official GitHub SDK</Paragraph>
<CodeBlock
code={`
client.defineJob({
id: "scheduled-job-1",
name: "Scheduled Job 1",
version: "0.1.1",
trigger: cronTrigger({
cron: "*/5 * * * *", // every 5 minutes
}),
run: async (payload, io, ctx) => {
//wrap an SDK call in io.runTask so it's resumable and displays in logs
const repo = await io.runTask(
"Get repo",
async () => {
//this is the regular GitHub SDK
const response = await octokit.rest.repos.get({
owner: "triggerdotdev",
repo: "trigger.dev",
});
return response.data;
},
//you can add metadata to the task to improve the display in the logs
{ name: "Get repo", icon: "github" }
);
},
});
`}
highlightedRanges={[[9, 22]]}
className="mb-4"
/>
<Header2 className="mb-2">How to use fetch</Header2>
<Paragraph spacing>
You can use the fetch API to make requests to any API. Or a different request library like
axios if you'd prefer. Again wrapping the request in a Task will make sure it's resumable.
</Paragraph>
<CodeBlock
code={`
client.defineJob({
id: "scheduled-job-1",
name: "Scheduled Job 1",
version: "0.1.1",
trigger: cronTrigger({
cron: "*/5 * * * *", // every 5 minutes
}),
run: async (payload, io, ctx) => {
//wrap anything in io.runTask so it's resumable and displays in logs
const repo = await io.runTask(
"Get org",
async () => {
//you can use fetch, axios, or any other library to make requests
const response = await fetch('https://api.github.com/orgs/nodejs');
return response.json();
},
//you can add metadata to the task to improve the display in the logs
{ name: "Get org", icon: "github" }
);
},
});
`}
highlightedRanges={[[9, 19]]}
className="mb-4"
/>
{api.examples && api.examples.length > 0 ? (
<>
<Header2 className="mb-2">Example {api.name} code</Header2>
<Paragraph spacing className="mb-4">
This is how you can use {api.name} with Trigger.dev. This code can be copied and
modified to suit your use-case.
</Paragraph>
{api.examples.length > 1 && (
<div className=" flex w-full flex-row gap-4 overflow-x-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
{api.examples?.map((example, index) => (
<button
onClick={() => changeCodeExample(index)}
key={example.codeUrl}
className={cn(
"w-64 min-w-[16rem] p-2 transition-colors duration-300 sm:w-full sm:rounded",
"border-px focus:border-px cursor-pointer border border-slate-900 bg-slate-900 text-slate-300 transition duration-300 hover:bg-slate-800 focus:border focus:border-indigo-600"
)}
>
{example.title}
</button>
))}
</div>
)}
<CodeExample example={api.examples[selectedExample]} />
</>
) : (
<>
<Header2 className="mb-2">Example code using fetch / an existing SDK</Header2>
<Paragraph spacing className="mb-4">
You can use one of our examples below as a starting point / reference for your projects.
Please{" "}
<Feedback
button={
<span className="cursor-pointer text-indigo-500 transition duration-300 hover:text-indigo-400">
reach out to us
</span>
}
defaultValue="help"
/>{" "}
if you're having any issues.
</Paragraph>
<div className=" flex w-full flex-row gap-4 overflow-x-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700 sm:h-full">
{fallbackExamples.map((example, index) => (
<button
onClick={() => changeCodeExample(index)}
key={example.codeUrl}
className={cn(
"w-64 min-w-[16rem] p-2 transition-colors duration-300 sm:w-full sm:rounded",
"border-px focus:border-px cursor-pointer border border-slate-900 bg-slate-900 text-slate-300 transition duration-300 hover:bg-slate-800 focus:border focus:border-indigo-600"
)}
>
{example.title}
</button>
))}
</div>
<CodeExample example={fallbackExamples[selectedExample]} />
</>
)}
</div>
);
}
@@ -1,14 +1,10 @@
import { useFetcher } from "@remix-run/react";
import React from "react";
import { Api } from "~/services/externalApis/apis";
import { Button } from "../primitives/Buttons";
import { Callout } from "../primitives/Callout";
import { Api } from "~/services/externalApis/apis.server";
import { Header1 } from "../primitives/Headers";
import { NamedIconInBox } from "../primitives/NamedIcon";
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet";
import { CustomHelp } from "./CustomHelp";
import { CheckIcon } from "@heroicons/react/24/solid";
import { useFetcher } from "@remix-run/react";
import { Paragraph } from "../primitives/Paragraph";
export function NoIntegrationSheet({
api,
@@ -31,27 +27,9 @@ export function NoIntegrationSheet({
<NamedIconInBox name={api.identifier} className="h-9 w-9" />
<Header1>{api.name}</Header1>
</div>
{requested ? (
<div className="flex items-center gap-1">
<CheckIcon className="h-4 w-4 text-green-500" />
<Paragraph variant="small">
We'll let you know when the Integration is available.
</Paragraph>
</div>
) : (
<fetcher.Form method="post" action={`/resources/apivote/${api.identifier}`}>
<Button
variant="primary/small"
disabled={isLoading}
LeadingIcon={isLoading ? "spinner-white" : undefined}
>
{isLoading ? "Saving…" : `I want an Integration for ${api.name}`}
</Button>
</fetcher.Form>
)}
</SheetHeader>
<SheetBody>
<CustomHelp name={api.name} />
<CustomHelp api={api} />
</SheetBody>
</SheetContent>
</Sheet>
+25 -11
View File
@@ -33,7 +33,19 @@ import { Spinner } from "../primitives/Spinner";
import type { DetailedTask } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route";
export function TaskDetail({ task }: { task: DetailedTask }) {
const { name, description, icon, status, params, properties, output, style, attempts } = task;
const {
name,
description,
icon,
status,
params,
properties,
output,
outputIsUndefined,
style,
attempts,
noop,
} = task;
const startedAt = task.startedAt ? new Date(task.startedAt) : undefined;
const completedAt = task.completedAt ? new Date(task.completedAt) : undefined;
@@ -140,16 +152,18 @@ export function TaskDetail({ task }: { task: DetailedTask }) {
<Paragraph variant="small">No input</Paragraph>
)}
</div>
<div className="mt-4 flex flex-col gap-2">
<Header3>Output</Header3>
{output ? (
<ClientOnly fallback={<Spinner />}>
{() => <CodeBlock code={output} maxLines={35} />}
</ClientOnly>
) : (
<Paragraph variant="small">No output</Paragraph>
)}
</div>
{!noop && (
<div className="mt-4 flex flex-col gap-2">
<Header3>Output</Header3>
{output && !outputIsUndefined ? (
<ClientOnly fallback={<Spinner />}>
{() => <CodeBlock code={output} maxLines={35} />}
</ClientOnly>
) : (
<Paragraph variant="small">No output</Paragraph>
)}
</div>
)}
</RunPanelBody>
</RunPanel>
);
+1 -1
View File
@@ -48,7 +48,7 @@ export async function $transaction<R>(
return await (prisma as PrismaClient).$transaction(fn, options);
} catch (error) {
if (isPrismaKnownError(error)) {
logger.debug("prisma.$transaction error", {
logger.error("prisma.$transaction error", {
code: error.code,
meta: error.meta,
stack: error.stack,
+4
View File
@@ -16,6 +16,8 @@ import {
OperatingSystemPlatform,
} from "./components/primitives/OperatingSystemProvider";
import { env } from "./env.server";
import { getSharedSqsEventConsumer } from "./services/events/sqsEventConsumer";
import { singleton } from "./utils/singleton";
const ABORT_DELAY = 30000;
@@ -190,3 +192,5 @@ function logError(error: unknown, request?: Request) {
}
console.error(error);
}
const sqsEventConsumer = singleton("sqsEventConsumer", getSharedSqsEventConsumer);
+17 -1
View File
@@ -11,7 +11,10 @@ const EnvironmentSchema = z.object({
SESSION_SECRET: z.string(),
MAGIC_LINK_SECRET: z.string(),
ENCRYPTION_KEY: z.string(),
WHITELISTED_EMAILS: z.string().refine(isValidRegex, "WHITELISTED_EMAILS must be a valid regex.").optional(),
WHITELISTED_EMAILS: z
.string()
.refine(isValidRegex, "WHITELISTED_EMAILS must be a valid regex.")
.optional(),
REMIX_APP_PORT: z.string().optional(),
LOGIN_ORIGIN: z.string().default("http://localhost:3030"),
APP_ORIGIN: z.string().default("http://localhost:3030"),
@@ -42,7 +45,20 @@ const EnvironmentSchema = z.object({
EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
WORKER_ENABLED: z.string().default("true"),
EXECUTION_WORKER_ENABLED: z.string().default("true"),
TASK_OPERATION_WORKER_ENABLED: z.string().default("true"),
TASK_OPERATION_WORKER_CONCURRENCY: z.coerce.number().int().default(10),
TASK_OPERATION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000),
/** Optional. Only used if you use the apps/proxy */
AWS_SQS_REGION: z.string().optional(),
/** Optional. Only used if you use the apps/proxy */
AWS_SQS_ACCESS_KEY_ID: z.string().optional(),
/** Optional. Only used if you use the apps/proxy */
AWS_SQS_SECRET_ACCESS_KEY: z.string().optional(),
/** Optional. Only used if you use the apps/proxy */
AWS_SQS_QUEUE_URL: z.string().optional(),
AWS_SQS_BATCH_SIZE: z.coerce.number().int().optional().default(10),
DISABLE_SSE: z.string().optional(),
});
export type Environment = z.infer<typeof EnvironmentSchema>;
+43
View File
@@ -0,0 +1,43 @@
import { useEffect, useState } from "react";
type EventSourceOptions = {
init?: EventSourceInit;
event?: string;
disabled?: boolean;
};
/**
* Subscribe to an event source and return the latest event.
* @param url The URL of the event source to connect to
* @param options The options to pass to the EventSource constructor
* @returns The last event received from the server
*/
export function useEventSource(
url: string | URL,
{ event = "message", init, disabled }: EventSourceOptions = {}
) {
const [data, setData] = useState<string | null>(null);
useEffect(() => {
if (disabled) {
return;
}
const eventSource = new EventSource(url, init);
eventSource.addEventListener(event ?? "message", handler);
// rest data if dependencies change
setData(null);
function handler(event: MessageEvent) {
setData(event.data || "UNKNOWN_EVENT_DATA");
}
return () => {
eventSource.removeEventListener(event ?? "message", handler);
eventSource.close();
};
}, [url, event, init, disabled]);
return data;
}
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { useEventSource } from "remix-utils/sse/react";
import { projectPath, projectStreamingPath } from "~/utils/pathBuilder";
import { useProject } from "./useProject";
import { useOrganization } from "./useOrganizations";
import { useNavigate } from "@remix-run/react";
import { useEventSource } from "./useEventSource";
export function useProjectSetupComplete() {
const project = useProject();
@@ -0,0 +1,22 @@
import { z } from "zod";
export const JobVersionDispatchableSchema = z.object({
type: z.literal("JOB_VERSION"),
id: z.string(),
});
export const DynamicTriggerDispatchableSchema = z.object({
type: z.literal("DYNAMIC_TRIGGER"),
id: z.string(),
});
export const EphemeralDispatchableSchema = z.object({
type: z.literal("EPHEMERAL"),
url: z.string(),
});
export const DispatchableSchema = z.discriminatedUnion("type", [
JobVersionDispatchableSchema,
DynamicTriggerDispatchableSchema,
EphemeralDispatchableSchema,
]);
+8 -4
View File
@@ -1,7 +1,10 @@
import type { JobRun, Task, TaskAttempt } from "@trigger.dev/database";
import { CachedTask, ServerTask } from "@trigger.dev/core";
export type TaskWithAttempts = Task & { attempts: TaskAttempt[]; run: JobRun };
export type TaskWithAttempts = Task & {
attempts: TaskAttempt[];
run: { forceYieldImmediately: boolean };
};
export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask {
return {
@@ -15,7 +18,8 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
status: task.status,
description: task.description,
params: task.params as any,
output: task.output as any,
output: task.outputIsUndefined ? undefined : (task.output as any),
context: task.context as any,
properties: task.properties as any,
style: task.style as any,
error: task.error,
@@ -31,7 +35,7 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
export type TaskForCaching = Pick<
Task,
"id" | "status" | "idempotencyKey" | "noop" | "output" | "parentId"
"id" | "status" | "idempotencyKey" | "noop" | "output" | "parentId" | "outputIsUndefined"
>;
export function prepareTasksForCaching(
@@ -104,7 +108,7 @@ function prepareTaskForCaching(task: TaskForCaching): CachedTask {
status: task.status,
idempotencyKey: task.idempotencyKey,
noop: task.noop,
output: task.output as any,
output: task.outputIsUndefined ? undefined : (task.output as any),
parentId: task.parentId,
};
}
+17 -20
View File
@@ -14,8 +14,7 @@ import { run as graphileRun, parseCronItems } from "graphile-worker";
import omit from "lodash.omit";
import { z } from "zod";
import { PrismaClient, PrismaClientOrTransaction } from "~/db.server";
import { PgListenService } from "~/services/db/pgListen.server";
import { workerLogger as logger } from "~/services/logger.server";
import { workerLogger as logger, trace } from "~/services/logger.server";
export interface MessageCatalogSchema {
[key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;
@@ -167,21 +166,6 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#runner?.events.on("pool:listen:success", async ({ workerPool, client }) => {
this.#logDebug("pool:listen:success");
// hijack client instance to listen and react to incoming NOTIFY events
const pgListen = new PgListenService(client, this.#name, logger);
await pgListen.on("trigger:graphile:migrate", async ({ latestMigration }) => {
this.#logDebug("Detected incoming migration", { latestMigration });
if (latestMigration > 10) {
// already migrated past v0.14 - nothing to do
return;
}
// simulate SIGTERM to trigger graceful shutdown
this._handleSignal("SIGTERM");
});
});
this.#runner?.events.on("pool:listen:error", ({ error }) => {
@@ -286,7 +270,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
spec,
});
const job = await this.#addJob(
const { job, durationInMs } = await this.#addJob(
identifier as string,
payload,
spec,
@@ -298,6 +282,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
payload,
spec,
job,
durationInMs,
});
return job;
@@ -320,6 +305,8 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
spec: TaskSpec,
tx: PrismaClientOrTransaction
) {
const now = performance.now();
const results = await tx.$queryRawUnsafe(
`SELECT * FROM ${this.graphileWorkerSchema}.add_job(
identifier => $1::text,
@@ -343,6 +330,8 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
spec.jobKeyMode || null
);
const durationInMs = performance.now() - now;
const rows = AddJobResultsSchema.safeParse(results);
if (!rows.success) {
@@ -353,7 +342,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
const job = rows.data[0];
return job as GraphileJob;
return { job: job as GraphileJob, durationInMs: Math.floor(durationInMs) };
}
async #removeJob(jobKey: string, tx: PrismaClientOrTransaction) {
@@ -487,7 +476,15 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
throw new Error(`No task for message type: ${String(typeName)}`);
}
await task.handler(payload, job);
await trace(
{
worker_job: job,
worker_name: this.#name,
},
async () => {
await task.handler(payload, job);
}
);
}
async #handleRecurringTask(
@@ -1,7 +1,7 @@
import { PrismaClient, prisma } from "~/db.server";
import { Project } from "~/models/project.server";
import { User } from "~/models/user.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
type EnvironmentSignalsMap = {
[x: string]: {
@@ -3,7 +3,7 @@ import { PrismaClient, prisma } from "~/db.server";
import { env } from "~/env.server";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import { Api, apisList } from "~/services/externalApis/apis";
import { Api, apisList } from "~/services/externalApis/apis.server";
import { integrationCatalog } from "~/services/externalApis/integrationCatalog.server";
import { Integration, OAuthClientSchema } from "~/services/externalApis/types";
import { getSecretStore } from "~/services/secrets/secretStore.server";
@@ -1,6 +1,6 @@
import { JobRun } from "@trigger.dev/database";
import { PrismaClient, prisma } from "~/db.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
export class RunStreamPresenter {
#prismaClient: PrismaClient;
@@ -1,5 +1,4 @@
import { RedactSchema } from "@trigger.dev/core";
import { StyleSchema } from "@trigger.dev/core";
import { RedactSchema, StyleSchema } from "@trigger.dev/core";
import { PrismaClient, prisma } from "~/db.server";
import { mergeProperties } from "~/utils/mergeProperties.server";
import { Redactor } from "~/utils/redactor";
@@ -58,6 +57,7 @@ export class TaskDetailsPresenter {
outputProperties: true,
params: true,
output: true,
outputIsUndefined: true,
error: true,
startedAt: true,
completedAt: true,
@@ -89,9 +89,11 @@ export class TaskDetailsPresenter {
return {
...task,
redact: undefined,
output: task.output
? JSON.stringify(this.#stringifyOutputWithRedactions(task.output, task.redact), null, 2)
: undefined,
output: JSON.stringify(
this.#stringifyOutputWithRedactions(task.output, task.redact),
null,
2
),
connection: task.runConnection,
params: task.params as Record<string, any>,
properties: mergeProperties(task.properties, task.outputProperties),
@@ -101,7 +103,7 @@ export class TaskDetailsPresenter {
#stringifyOutputWithRedactions(output: any, redact: unknown): any {
if (!output) {
return;
return output;
}
const parsedRedact = RedactSchema.safeParse(redact);
+13 -6
View File
@@ -1,19 +1,26 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { getUsersInvites } from "~/models/member.server";
import { SelectBestProjectPresenter } from "~/presenters/SelectBestProjectPresenter.server";
import { requireUserId } from "~/services/session.server";
import { newOrganizationPath, projectPath } from "~/utils/pathBuilder";
import { requireUser } from "~/services/session.server";
import { invitesPath, newOrganizationPath, projectPath } from "~/utils/pathBuilder";
//this loader chooses the best project to redirect you to, ideally based on the cookie
export const loader = async ({ request }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
const user = await requireUser(request);
//if there are invites then we should redirect to the invites page
const invites = await getUsersInvites({ email: user.email });
if (invites.length > 0) {
return redirect(invitesPath());
}
const presenter = new SelectBestProjectPresenter();
try {
const { project, organization } = await presenter.call({ userId, request });
const { project, organization } = await presenter.call({ userId: user.id, request });
//redirect them to the most appropriate project
return redirect(projectPath(organization, project));
} catch (e) {
//this should only happen if the user has no projects
//this should only happen if the user has no projects, and no invites
return redirect(newOrganizationPath());
}
};
@@ -82,7 +82,7 @@ export default function Integrations() {
<PageContainer>
<PageHeader>
<PageTitleRow>
<PageTitle title="Integrations" />
<PageTitle title="Integrations & APIs" />
<PageButtons>
<LinkButton
to={docsPath("/integrations/introduction")}
@@ -2,7 +2,7 @@ import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useFetcher, useRevalidator } from "@remix-run/react";
import { useEffect } from "react";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import { InlineCode } from "~/components/code/InlineCode";
import {
EndpointIndexStatusIcon,
@@ -51,7 +51,7 @@ export function FirstEndpointSheet({ projectId, environments }: FirstEndpointShe
return (
<Sheet>
<SheetTrigger>
<Button variant="secondary/medium">Add your first endpoint</Button>
<ButtonContent variant="secondary/medium">Add your first endpoint</ButtonContent>
</SheetTrigger>
<SheetContent size="lg">
<SheetHeader>
@@ -2,7 +2,7 @@ import { useRevalidator } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { useEffect, useMemo, useState } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import {
EndpointIndexStatusIcon,
EndpointIndexStatusLabel,
@@ -1,6 +1,5 @@
import { Await, useLoaderData } from "@remix-run/react";
import { LoaderFunctionArgs, SerializeFrom, defer } from "@remix-run/server-runtime";
import { Suspense } from "react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
import { Spinner } from "~/components/primitives/Spinner";
import { TaskDetail } from "~/components/run/TaskDetail";
import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
@@ -12,26 +11,28 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { taskParam } = TaskParamsSchema.parse(params);
const presenter = new TaskDetailsPresenter();
const taskPromise = presenter.call({
const task = await presenter.call({
userId,
id: taskParam,
});
return defer({
taskPromise,
return typedjson({
task,
});
};
export type DetailedTask = NonNullable<Awaited<SerializeFrom<typeof loader>["taskPromise"]>>;
export type DetailedTask = NonNullable<UseDataFunctionReturn<typeof loader>["task"]>;
export default function Page() {
const { taskPromise } = useLoaderData<typeof loader>();
const { task } = useTypedLoaderData<typeof loader>();
return (
<Suspense fallback={<Spinner />}>
<Await resolve={taskPromise} errorElement={<p>Error loading task!</p>}>
{(resolvedTask) => resolvedTask && <TaskDetail task={resolvedTask as any} />}
</Await>
</Suspense>
);
if (!task) {
return (
<div className="flex h-full items-center justify-center">
<Spinner />
</div>
);
}
return <TaskDetail task={task} />;
}
@@ -2,7 +2,7 @@ import { useRevalidator } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { Fragment, useEffect } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
import { RunOverview } from "~/components/run/RunOverview";
@@ -67,6 +67,7 @@ export default function Page() {
const revalidator = useRevalidator();
const events = useEventSource(runStreamingPath(organization, project, job, run), {
event: "message",
disabled: !!run.completedAt,
});
useEffect(() => {
if (events !== null) {
@@ -1,6 +1,5 @@
import { useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { ClipboardIcon } from "@heroicons/react/20/solid";
import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
import { Form, useActionData, useSubmit } from "@remix-run/react";
import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
@@ -33,10 +32,14 @@ import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/mode
import { TestJobPresenter } from "~/presenters/TestJobPresenter.server";
import { TestJobService } from "~/services/jobs/testJob.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import { isValidIcon } from "~/utils/icon";
import { JobParamsSchema, jobRunDashboardPath, trimTrailingSlash } from "~/utils/pathBuilder";
import {
JobParamsSchema,
docsPath,
jobRunDashboardPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -139,7 +142,7 @@ export default function Page() {
setDefaultJson(code);
}, []);
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string>(environments[0].id);
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(environments.at(0)?.id);
const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId);
const currentJson = useRef<string>(defaultJson);
@@ -147,6 +150,10 @@ export default function Page() {
const submitForm = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
if (!selectedEnvironmentId) {
return;
}
submit(
{
payload: currentJson.current,
@@ -175,10 +182,33 @@ export default function Page() {
if (environments.length === 0) {
return (
<Callout variant="warning">
Can't run a test when there are no environments. This shouldn't happen, please contact
support.
</Callout>
<div className="flex flex-col gap-4">
<Callout variant="info">
There are no environments that you can test this job with you can't run Tests against
your teammates' Dev environments. You should run the code locally (using the CLI) so that
this Job will be associated with your Dev environment. This also means that this Job
hasn't been deployed to Staging or Prod yet.
</Callout>
<div>
<Header2 spacing>Useful guides</Header2>
<div className="flex gap-2">
<LinkButton
to={docsPath("documentation/guides/cli#dev-command")}
variant="secondary/small"
LeadingIcon="docs"
>
Using the CLI
</LinkButton>
<LinkButton
to={docsPath("documentation/guides/deployment")}
variant="secondary/small"
LeadingIcon="docs"
>
Deploying your Jobs
</LinkButton>
</div>
</div>
</div>
);
}
@@ -1,6 +1,5 @@
import { Await, useLoaderData } from "@remix-run/react";
import { LoaderFunctionArgs, defer } from "@remix-run/server-runtime";
import { Suspense } from "react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { Spinner } from "~/components/primitives/Spinner";
import { TaskDetail } from "~/components/run/TaskDetail";
import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
@@ -12,24 +11,26 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { taskParam } = TriggerSourceRunTaskParamsSchema.parse(params);
const presenter = new TaskDetailsPresenter();
const taskPromise = presenter.call({
const task = await presenter.call({
userId,
id: taskParam,
});
return defer({
taskPromise,
return typedjson({
task,
});
};
export default function Page() {
const { taskPromise } = useLoaderData<typeof loader>();
const { task } = useTypedLoaderData<typeof loader>();
return (
<Suspense fallback={<Spinner />}>
<Await resolve={taskPromise} errorElement={<p>Error loading task!</p>}>
{(resolvedTask) => resolvedTask && <TaskDetail task={resolvedTask as any} />}
</Await>
</Suspense>
);
if (!task) {
return (
<div className="flex h-full items-center justify-center">
<Spinner />
</div>
);
}
return <TaskDetail task={task} />;
}
@@ -2,7 +2,7 @@ import { useRevalidator } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { Fragment, useEffect } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
import { RunOverview } from "~/components/run/RunOverview";
@@ -0,0 +1,59 @@
import path from "path";
import os from "os";
import fs from "fs";
import v8 from "v8";
import { PassThrough } from "stream";
import { json, type DataFunctionArgs } from "@remix-run/node";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { requireUser } from "~/services/session.server";
// Format date as yyyy-MM-dd HH_mm_ss_SSS
function formatDate(date: Date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const milliseconds = date.getMilliseconds();
return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")} ${hours
.toString()
.padStart(2, "0")}_${minutes.toString().padStart(2, "0")}_${seconds
.toString()
.padStart(2, "0")}_${milliseconds.toString().padStart(3, "0")}`;
}
export async function loader({ request }: DataFunctionArgs) {
const user = await requireUser(request);
if (!user.admin) {
throw new Response("You must be an admin to perform this action", { status: 403 });
}
const host = request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");
const tempDir = os.tmpdir();
const filepath = path.join(tempDir, `${host}-${formatDate(new Date())}.heapsnapshot`);
const snapshotPath = v8.writeHeapSnapshot(filepath);
if (!snapshotPath) {
throw new Response("No snapshot saved", { status: 500 });
}
const body = new PassThrough();
const stream = fs.createReadStream(snapshotPath);
stream.on("open", () => stream.pipe(body));
stream.on("error", (err) => body.end(err));
stream.on("end", () => body.end());
return new Response(body as any, {
status: 200,
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${path.basename(snapshotPath)}"`,
"Content-Length": (await fs.promises.stat(snapshotPath)).size.toString(),
},
});
}
@@ -0,0 +1,59 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import {
EphemeralEventDispatcherRequestBodySchema,
InvokeJobRequestBodySchema,
} from "@trigger.dev/core";
import { z } from "zod";
import { PrismaErrorSchema } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CreateEphemeralEventDispatcherService } from "~/services/dispatchers/createEphemeralEventDispatcher.server";
import { InvokeJobService } from "~/services/jobs/invokeJob.server";
import { logger } from "~/services/logger.server";
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
}
// Now parse the request body
const anyBody = await request.json();
logger.debug("CreateEphemeralEventDispatcherService.call() request body", {
body: anyBody,
});
const body = EphemeralEventDispatcherRequestBodySchema.safeParse(anyBody);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
const service = new CreateEphemeralEventDispatcherService();
try {
const dispatcher = await service.call(authenticationResult.environment, body.data);
if (!dispatcher) {
return json({ error: "Could not create Event Dispatcher" }, { status: 500 });
}
return json({ id: dispatcher.id });
} catch (error) {
const prismaError = PrismaErrorSchema.safeParse(error);
// Record not found in the database
if (prismaError.success && prismaError.data.code === "P2005") {
return json({ error: "Dispatcher not found" }, { status: 404 });
} else {
return json({ error: "Internal Server Error" }, { status: 500 });
}
}
}
@@ -0,0 +1,49 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { SendBulkEventsBodySchema } from "@trigger.dev/core";
import { generateErrorMessage } from "zod-error";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { IngestSendEvent } from "~/services/events/ingestSendEvent.server";
import { eventRecordToApiJson } from "~/api.server";
import { EventRecord } from "@trigger.dev/database";
export async function action({ request }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const authenticatedEnv = authenticationResult.environment;
// Now parse the request body
const anyBody = await request.json();
const body = SendBulkEventsBodySchema.safeParse(anyBody);
if (!body.success) {
return json({ message: generateErrorMessage(body.error.issues) }, { status: 422 });
}
const service = new IngestSendEvent();
const events: EventRecord[] = [];
for (const event of body.data.events) {
const eventRecord = await service.call(authenticatedEnv, event, body.data.options);
if (!eventRecord) {
return json({ error: "Failed to create event during bulk ingest" }, { status: 500 });
}
events.push(eventRecord);
}
return json(events.map(eventRecordToApiJson));
}
@@ -191,7 +191,8 @@ export class CompleteRunTaskService {
},
data: {
status: "COMPLETED",
output: taskBody.output ?? undefined,
output: taskBody.output as any,
outputIsUndefined: typeof taskBody.output === "undefined",
completedAt: new Date(),
outputProperties: taskBody.properties,
},
@@ -1,22 +1,17 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { TaskStatus } from "@trigger.dev/database";
import {
API_VERSIONS,
RunTaskBodyOutput,
RunTaskBodyOutputSchema,
RunTaskResponseWithCachedTasksBody,
ServerTask,
} from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { prepareTasksForCaching, taskWithAttemptsToServerTask } from "~/models/task.server";
import { PrismaClient, prisma } from "~/db.server";
import { prepareTasksForCaching } from "~/models/task.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ulid } from "~/services/ulid.server";
import { workerQueue } from "~/services/worker.server";
import { generateSecret } from "~/services/sources/utils.server";
import { env } from "~/env.server";
import { RunTaskService } from "~/services/tasks/runTask.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -161,184 +156,3 @@ class ChangeRequestLazyLoadedCachedTasks {
};
}
}
export class RunTaskService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
runId: string,
idempotencyKey: string,
taskBody: RunTaskBodyOutput
): Promise<ServerTask | undefined> {
const task = await $transaction(this.#prismaClient, async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
runId_idempotencyKey: {
runId,
idempotencyKey,
},
},
include: {
attempts: true,
run: true,
},
});
const delayUntilInFuture = taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now();
const callbackEnabled = taskBody.callback?.enabled;
if (existingTask) {
if (existingTask.status === "CANCELED") {
const existingTaskStatus =
delayUntilInFuture || callbackEnabled || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
const resumedExistingTask = await tx.task.update({
where: {
id: existingTask.id,
},
data: {
status: existingTaskStatus,
startedAt: new Date(),
completedAt: existingTaskStatus === "COMPLETED" ? new Date() : undefined,
},
include: {
run: true,
attempts: true,
},
});
return resumedExistingTask;
}
return existingTask;
}
const run = await tx.jobRun.findUnique({
where: {
id: runId,
},
select: {
status: true,
},
});
if (!run) throw new Error("Run not found");
// If task.delayUntil is set and is in the future, we'll set the task's status to "WAITING", else set it to RUNNING
let status: TaskStatus;
if (run.status === "CANCELED") {
status = "CANCELED";
} else {
status =
delayUntilInFuture || callbackEnabled || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
}
const taskId = ulid();
const callbackUrl = callbackEnabled
? `${env.APP_ORIGIN}/api/v1/tasks/${taskId}/callback/${generateSecret(12)}`
: undefined;
const task = await tx.task.create({
data: {
id: taskId,
idempotencyKey,
displayKey: taskBody.displayKey,
runConnection: taskBody.connectionKey
? {
connect: {
runId_key: {
runId,
key: taskBody.connectionKey,
},
},
}
: undefined,
icon: taskBody.icon,
run: {
connect: {
id: runId,
},
},
parent: taskBody.parentId ? { connect: { id: taskBody.parentId } } : undefined,
name: taskBody.name ?? "Task",
description: taskBody.description,
status,
startedAt: new Date(),
completedAt: status === "COMPLETED" || status === "CANCELED" ? new Date() : undefined,
noop: taskBody.noop,
delayUntil: taskBody.delayUntil,
params: taskBody.params ?? undefined,
properties: this.#filterProperties(taskBody.properties) ?? undefined,
redact: taskBody.redact ?? undefined,
operation: taskBody.operation,
callbackUrl,
style: taskBody.style ?? { style: "normal" },
childExecutionMode: taskBody.parallel ? "PARALLEL" : "SEQUENTIAL",
attempts: {
create: {
number: 1,
status: "PENDING",
},
},
},
include: {
run: true,
attempts: true,
},
});
if (task.status === "RUNNING" && typeof taskBody.operation === "string") {
// We need to schedule the operation
await workerQueue.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ tx, runAt: task.delayUntil ?? undefined, jobKey: `operation:${task.id}` }
);
} else if (task.status === "WAITING" && callbackUrl && taskBody.callback) {
if (taskBody.callback.timeoutInSeconds > 0) {
// We need to schedule the callback timeout
await workerQueue.enqueue(
"processCallbackTimeout",
{
id: task.id,
},
{
tx,
runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000),
jobKey: `process-callback:${task.id}`,
}
);
}
}
return task;
});
return task ? taskWithAttemptsToServerTask(task) : undefined;
}
#filterProperties(properties: RunTaskBodyOutput["properties"]): RunTaskBodyOutput["properties"] {
if (!properties) return;
return properties.filter((property) => {
if (!property) return false;
return typeof property.label === "string" && typeof property.text === "string";
});
}
}
@@ -0,0 +1,49 @@
import { useFetcher } from "@remix-run/react";
import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { useEffect } from "react";
import invariant from "tiny-invariant";
import { CodeBlock } from "~/components/code/CodeBlock";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Spinner } from "~/components/primitives/Spinner";
import { ApiExample } from "~/services/externalApis/apis.server";
import { requireUser } from "~/services/session.server";
export async function loader({ request }: LoaderFunctionArgs) {
await requireUser(request);
const url = new URL(request.url);
const codeUrl = url.searchParams.get("url");
invariant(typeof codeUrl === "string", "codeUrl is required");
const decodedCodeUrl = decodeURIComponent(codeUrl);
const response = await fetch(decodedCodeUrl);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const code = await response.text();
const hideCodeRegex = /(\n)?\/\/ hide-code[\s\S]*?\/\/ end-hide-code(\n)*/gm;
const cleanedCode = code?.replace(hideCodeRegex, "\n");
return json({
code: cleanedCode,
});
}
export function CodeExample({ example }: { example: ApiExample }) {
const customerFetcher = useFetcher<typeof loader>();
useEffect(() => {
customerFetcher.load(`/resources/codeexample?url=${encodeURIComponent(example.codeUrl)}`);
}, [example.codeUrl]);
if (customerFetcher.state === "loading")
return (
<div className="flex h-96 w-full items-center justify-center gap-2 rounded-md border border-slate-800 font-mono">
<Spinner />
<Paragraph>Loading example code</Paragraph>
</div>
);
return (
customerFetcher.data && <CodeBlock code={customerFetcher.data.code ?? ""} className="mt-2" />
);
}
@@ -2,7 +2,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
export async function loader({ request, params }: LoaderFunctionArgs) {
await requireUserId(request);
@@ -2,7 +2,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
export async function loader({ request, params }: LoaderFunctionArgs) {
await requireUserId(request);
+1 -1
View File
@@ -1,7 +1,7 @@
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { logger } from "~/services/logger.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
+1 -1
View File
@@ -1,6 +1,6 @@
import { useLoaderData } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import { z } from "zod";
export async function loader({ request }: LoaderFunctionArgs) {
+17 -1
View File
@@ -20,7 +20,19 @@ export async function authenticateApiRequest(
request: Request,
{ allowPublicKey = false }: { allowPublicKey?: boolean } = {}
): Promise<ApiAuthenticationResult | undefined> {
const result = getApiKeyFromRequest(request);
const apiKey = getApiKeyFromRequest(request);
if (!apiKey) {
return;
}
return authenticateApiKey(apiKey, { allowPublicKey });
}
export async function authenticateApiKey(
apiKey: string,
{ allowPublicKey = false }: { allowPublicKey?: boolean } = {}
): Promise<ApiAuthenticationResult | undefined> {
const result = getApiKeyResult(apiKey);
if (!result) {
return;
@@ -69,6 +81,10 @@ export function getApiKeyFromRequest(request: Request) {
}
const apiKey = authorization.data.replace(/^Bearer /, "");
return apiKey;
}
export function getApiKeyResult(apiKey: string) {
const type = isPublicApiKey(apiKey) ? ("PUBLIC" as const) : ("PRIVATE" as const);
return { apiKey, type };
}
@@ -0,0 +1,68 @@
import { EphemeralEventDispatcherRequestBody } from "@trigger.dev/core";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { ExpireDispatcherService } from "./expireDispatcher.server";
export class CreateEphemeralEventDispatcherService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
environment: AuthenticatedEnvironment,
data: EphemeralEventDispatcherRequestBody
) {
return await $transaction(this.#prismaClient, async (tx) => {
const existingDispatcher = await tx.eventDispatcher.findUnique({
where: {
dispatchableId_environmentId: {
dispatchableId: data.url,
environmentId: environment.id,
},
},
});
if (existingDispatcher) {
return existingDispatcher;
}
const externalAccount = data.accountId
? await this.#prismaClient.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: data.accountId,
},
},
create: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: data.accountId,
},
update: {},
})
: undefined;
const dispatcher = await tx.eventDispatcher.create({
data: {
dispatchableId: data.url,
environmentId: environment.id,
source: data.source ?? "trigger.dev",
payloadFilter: data.filter,
contextFilter: data.contextFilter,
dispatchable: { url: data.url, type: "EPHEMERAL" },
enabled: true,
event: typeof data.name === "string" ? [data.name] : data.name,
manual: false,
externalAccountId: externalAccount?.id,
},
});
await ExpireDispatcherService.enqueue(dispatcher.id, data.timeoutInSeconds, tx);
return dispatcher;
});
}
}
@@ -0,0 +1,36 @@
import { PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
export class ExpireDispatcherService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(id: string) {
await this.#prismaClient.eventDispatcher.delete({
where: {
id,
},
});
}
static async dequeue(id: string, tx?: PrismaClientOrTransaction) {
await workerQueue.dequeue(`expire:${id}`, { tx });
}
static async enqueue(id: string, timeoutInSeconds: number, tx?: PrismaClientOrTransaction) {
await workerQueue.enqueue(
"expireDispatcher",
{
id,
},
{
tx,
runAt: new Date(Date.now() + 1000 * timeoutInSeconds),
jobKey: `expire:${id}`,
}
);
}
}
@@ -0,0 +1,98 @@
import { PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { taskOperationWorker } from "../worker.server";
import { EphemeralDispatchableSchema } from "~/models/eventDispatcher.server";
import { ExpireDispatcherService } from "./expireDispatcher.server";
export class InvokeEphemeralDispatcherService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(id: string, eventRecordId: string) {
const eventDispatcher = await this.#prismaClient.eventDispatcher.findUnique({
where: {
id,
},
});
if (!eventDispatcher) {
return;
}
if (!eventDispatcher.enabled) {
return;
}
const eventRecord = await this.#prismaClient.eventRecord.findUnique({
where: {
id: eventRecordId,
},
include: {
externalAccount: true,
},
});
if (!eventRecord) {
return;
}
if (eventRecord.cancelledAt) {
return;
}
const dispatchable = EphemeralDispatchableSchema.safeParse(eventDispatcher.dispatchable);
if (!dispatchable.success) {
return;
}
const url = dispatchable.data.url;
const body = {
id: eventRecord.eventId,
source: eventRecord.source,
name: eventRecord.name,
payload: eventRecord.payload,
context: eventRecord.context,
timestamp: eventRecord.timestamp,
accountId: eventRecord.externalAccount ? eventRecord.externalAccount.identifier : undefined,
};
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(
`Failed to invoke ephemeral dispatcher: ${response.statusText} [${response.status}]`
);
}
// Run the expire dispatcher service
await ExpireDispatcherService.enqueue(id, 0);
}
static async dequeue(id: string, tx?: PrismaClientOrTransaction) {
await taskOperationWorker.dequeue(`invoke:ephemeral:${id}`, { tx });
}
static async enqueue(id: string, eventRecordId: string, tx?: PrismaClientOrTransaction) {
await taskOperationWorker.enqueue(
"invokeEphemeralDispatcher",
{
id,
eventRecordId,
},
{
tx,
jobKey: `invoke:ephemeral:${id}`,
}
);
}
}
@@ -3,6 +3,7 @@ import {
ConnectionAuth,
EndpointHeadersSchema,
ErrorWithStackSchema,
ExecuteJobHeadersSchema,
HttpSourceResponseSchema,
IndexEndpointResponseSchema,
NormalizedResponseSchema,
@@ -14,6 +15,7 @@ import {
RegisterTriggerBodyV1,
RunJobBody,
RunJobResponseSchema,
RunNotification,
ValidateResponse,
ValidateResponseSchema,
} from "@trigger.dev/core";
@@ -148,6 +150,7 @@ export class EndpointApi {
response,
parser: RunJobResponseSchema,
errorParser: ErrorWithStackSchema,
headersParser: ExecuteJobHeadersSchema,
durationInMs: Math.floor(performance.now() - startTimeInMs),
};
}
@@ -367,6 +370,20 @@ export class EndpointApi {
durationInMs: Math.floor(performance.now() - startTimeInMs),
};
}
async deliverRunNotification(notification: RunNotification<any>) {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trigger-api-key": this.apiKey,
"x-trigger-action": "RUN_NOTIFICATION",
},
body: JSON.stringify(notification),
});
return response;
}
}
async function safeFetch(url: string, options: RequestInit) {
@@ -96,6 +96,13 @@ export class DeliverEventService {
return true;
}
if (
dispatcher.externalAccountId &&
dispatcher.externalAccountId !== eventRecord.externalAccountId
) {
return false;
}
const payloadFilter = EventFilterSchema.safeParse(dispatcher.payloadFilter ?? {});
const contextFilter = EventFilterSchema.safeParse(dispatcher.contextFilter ?? {});
@@ -3,21 +3,8 @@ import type { PrismaClientOrTransaction } from "~/db.server";
import { prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { CreateRunService } from "~/services/runs/createRun.server";
const JobVersionDispatchableSchema = z.object({
type: z.literal("JOB_VERSION"),
id: z.string(),
});
const DynamicTriggerDispatchableSchema = z.object({
type: z.literal("DYNAMIC_TRIGGER"),
id: z.string(),
});
const DispatchableSchema = z.discriminatedUnion("type", [
JobVersionDispatchableSchema,
DynamicTriggerDispatchableSchema,
]);
import { InvokeEphemeralDispatcherService } from "../dispatchers/invokeEphemeralEventDispatcher.server";
import { DispatchableSchema } from "~/models/eventDispatcher.server";
export class InvokeDispatcherService {
#prismaClient: PrismaClientOrTransaction;
@@ -142,6 +129,11 @@ export class InvokeDispatcherService {
});
}
break;
}
case "EPHEMERAL": {
await InvokeEphemeralDispatcherService.enqueue(eventDispatcher.id, eventRecord.id);
break;
}
}
@@ -0,0 +1,151 @@
import { Consumer } from "sqs-consumer";
import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { logger, trace } from "../logger.server";
import { Message, SQSClient } from "@aws-sdk/client-sqs";
import { authenticateApiKey } from "../apiAuth.server";
import { SendEventBodySchema } from "@trigger.dev/core";
import { z } from "zod";
import { fromZodError } from "zod-validation-error";
import { IngestSendEvent } from "./ingestSendEvent.server";
import { env } from "~/env.server";
import { singleton } from "~/utils/singleton";
type SqsEventConsumerOptions = {
queueUrl: string;
/** This cannot be higher than the AWS limit of 10. */
batchSize: number;
region: string;
accessKeyId: string;
secretAccessKey: string;
};
const messageSchema = SendEventBodySchema.extend({
apiKey: z.string(),
});
export class SqsEventConsumer {
readonly #ingestEventService: IngestSendEvent;
readonly #consumer: Consumer;
constructor(
readonly prismaClient: PrismaClientOrTransaction = prisma,
options: SqsEventConsumerOptions
) {
this.#ingestEventService = new IngestSendEvent();
logger.debug("SqsEventConsumer starting", {
queueUrl: options.queueUrl,
region: options.region,
});
this.#consumer = Consumer.create({
queueUrl: options.queueUrl,
batchSize: options.batchSize,
sqs: new SQSClient({
region: options.region,
credentials: {
accessKeyId: options.accessKeyId,
secretAccessKey: options.secretAccessKey,
},
}),
handleMessage: async (message) => {
await trace({ sqsMessage: message }, async () => await this.#processEvent(message));
},
});
this.#consumer.on("error", (err, message) => {
logger.error("SqsEventConsumer error", { error: err.message, sqsMessage: message });
//todo what do we want to do here?
});
this.#consumer.on("processing_error", (err, message) => {
logger.error("SqsEventConsumer processing_error", {
error: err.message,
sqsMessage: message,
});
//todo what do we want to do here?
});
this.#consumer.on("timeout_error", (err, message) => {
logger.error("SqsEventConsumer timeout_error", { error: err.message, sqsMessage: message });
//todo what do we want to do here?
});
//Stop the consumer if the process is terminated
process.on("SIGTERM", () => {
this.stop();
});
this.#consumer.start();
}
public stop() {
logger.debug("SqsEventConsumer stopping");
this.#consumer.stop({ abort: true });
}
async #processEvent(message: Message) {
logger.debug("SqsEventConsumer processing event");
//parse the body
if (!message.Body) {
logger.error("SqsEventConsumer message has no body");
return;
}
const body = messageSchema.safeParse(JSON.parse(message.Body));
if (!body.success) {
logger.error("SqsEventConsumer message body is invalid", {
error: fromZodError(body.error).message,
});
return;
}
//authenticate API Key
const authenticationResult = await authenticateApiKey(body.data.apiKey);
if (!authenticationResult) {
logger.warn("SqsEventConsumer message has invalid API key");
return;
}
const authenticatedEnv = authenticationResult.environment;
logger.info("sqs_event", { event: body.data.event, options: body.data.options });
const event = await this.#ingestEventService.call(
authenticatedEnv,
body.data.event,
body.data.options
);
if (!event) {
logger.error("SqsEventConsumer failed to create event");
return;
}
logger.debug("SqsEventConsumer processed event", { event });
}
}
export function getSharedSqsEventConsumer() {
if (
env.AWS_SQS_QUEUE_URL &&
env.AWS_SQS_REGION &&
env.AWS_SQS_ACCESS_KEY_ID &&
env.AWS_SQS_SECRET_ACCESS_KEY
) {
const consumer = new SqsEventConsumer(undefined, {
queueUrl: env.AWS_SQS_QUEUE_URL,
batchSize: env.AWS_SQS_BATCH_SIZE,
region: env.AWS_SQS_REGION,
accessKeyId: env.AWS_SQS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY,
});
return consumer;
}
console.log(
"The SqsEventConsumer is disabled because AWS credentials are missing. This is OK as this is an optional feature."
);
}
@@ -0,0 +1,54 @@
import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { logger } from "../logger.server";
export type CreateExecutionEventInput = {
organizationId: string;
projectId: string;
environmentId: string;
jobId: string;
runId: string;
eventTime: Date;
eventType: "start" | "finish";
drift?: number;
};
export class CreateExecutionEventService {
constructor(private prismaClient: PrismaClientOrTransaction = prisma) {}
public async call(input: CreateExecutionEventInput) {
await this.prismaClient.$executeRaw`
INSERT INTO "triggerdotdev_events"."run_executions" (
"organization_id",
"project_id",
"environment_id",
"job_id",
"run_id",
"event_time",
"event_type",
"drift_amount_in_ms"
) VALUES (
${input.organizationId},
${input.projectId},
${input.environmentId},
${input.jobId},
${input.runId},
${input.eventTime},
${input.eventType === "start" ? 1 : -1},
${input.drift}
)
`;
}
}
export async function createExecutionEvent(
input: CreateExecutionEventInput,
options?: { prismaClient?: PrismaClientOrTransaction }
) {
const service = new CreateExecutionEventService(options?.prismaClient);
try {
return await service.call(input);
} catch (error) {
logger.error("Error creating execution event", { error });
}
}
@@ -0,0 +1,804 @@
export type Api = {
identifier: string;
name: string;
examples?: ApiExample[];
};
export type ApiExample = {
title: string;
version: string;
codeUrl: string;
slug: string;
};
export const apisList = [
{
identifier: "airtable",
name: "Airtable",
examples: [
{
title: "Update Airtable when a new subscription is added to Stripe.",
slug: "stripe-sub-update-airtable",
version: "1.0.0",
codeUrl:
"https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/stripeNewSubscriptionUpdateAirtable.ts",
},
{
title: "Add a new record to Airtable when a Typeform response is submitted.",
version: "1.0.0",
slug: "new-airtable-record-from-typeform",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/typeformNewSubmissionUpdateAirtable.ts",
},
{
title: "Update Airtable database when there is a sale in Stripe.",
version: "1.0.0",
slug: "update-airtable-when-stripe-account-updated",
codeUrl:
"https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/syncStripeWithAirtable.ts",
},
],
},
{
identifier: "algolia",
name: "Algolia",
},
{
identifier: "anthropic",
name: "Anthropic",
},
{
identifier: "appsmith",
name: "Appsmith",
},
{
identifier: "appwrite",
name: "Appwrite",
},
{
identifier: "asana",
name: "Asana",
examples: [
{
title: "Get user details from Asana",
slug: "get-user-details",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/asana.ts",
},
],
},
{
identifier: "atlassian",
name: "Atlassian",
},
{
identifier: "aws",
name: "AWS",
examples: [
{
title: "Trigger an AWS Lambda function with a defined payload and log the results.",
slug: "get-user-details",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/aws.ts",
},
],
},
{
identifier: "brex",
name: "Brex",
examples: [
{
title: "Create a new title in a Brex account.",
slug: "create-new-brex-title",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/brex.ts",
},
],
},
{
identifier: "caldotcom",
name: "Cal.com",
examples: [
{
title: "Send a Slack message when meetings are booked or cancelled.",
slug: "cal-slack-meeting-alert",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/cal-http-endpoint.ts",
},
{
title: "Find all Cal.com bookings for a user.",
slug: "cal-find-bookings",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/cal.ts",
},
],
},
{
identifier: "clerk",
name: "Clerk",
},
{
identifier: "clickup",
name: "ClickUp",
},
{
identifier: "coda",
name: "Coda",
},
{
identifier: "crowddotdev",
name: "Crowd.dev",
},
{
identifier: "deepl",
name: "DeepL",
examples: [
{
title: "Translate some text with DeepL.",
slug: "translate-text-with-deepl",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/deepl.ts",
},
],
},
{
identifier: "discord",
name: "Discord",
examples: [
{
title: "Create a Discord bot and send a message to a channel.",
slug: "discord-bot-send-message",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/discord.ts",
},
],
},
{
identifier: "documenso",
name: "Documenso",
},
{
identifier: "dropbox",
name: "Dropbox",
},
{
identifier: "facebook",
name: "Facebook",
},
{
identifier: "fastify",
name: "Fastify",
},
{
identifier: "flickr",
name: "Flickr",
},
{
identifier: "github",
name: "GitHub",
examples: [
{
title: "Send a message to a Slack channel when a repo is starred.",
slug: "github-star-to-slack",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubNewStarToSlack.ts",
},
{
title: "Create a Linear issue when a pull request is opened on a GitHub repo.",
slug: "linear-ticket-on-github-pr",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearCreateIssueOnPR.ts",
},
{
title:
"Send a reminder message to a Slack channel if a GitHub issue is left open for 24 hours.",
slug: "github-issue-reminder",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubIssueReminder.ts",
},
{
title: "Add a custom label to a GitHub issue.",
slug: "github-custom-label",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubNewIssueOpened.ts",
},
],
},
{
identifier: "giphy",
name: "Giphy",
},
{
identifier: "gmail",
name: "Gmail",
examples: [
{
title: "Send an email using Gmail.",
slug: "send-email-with-gmail",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/gmail.ts",
},
],
},
{
identifier: "googlecalendar",
name: "Google Calendar",
examples: [
{
title: "Create a new Google Calendar event",
slug: "create-google-calendar-event",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/google-calendar.ts",
},
],
},
{
identifier: "googledocs",
name: "Google Docs",
},
{
identifier: "googledrive",
name: "Google Drive",
examples: [
{
title: "Update a filename in Google Drive.",
slug: "update-google-drive-filename",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/google-drive.ts",
},
],
},
{
identifier: "googlemaps",
name: "Google Maps",
examples: [
{
title: "Make a geocode request with Google Maps.",
slug: "google-maps-geocode",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/google-maps.ts",
},
],
},
{
identifier: "googlesheets",
name: "Google Sheets",
examples: [
{
title: "Insert data into a row in Google Sheets.",
slug: "insert-data-into-google-sheets",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/google-sheets.ts",
},
],
},
{
identifier: "hubspot",
name: "HubSpot",
examples: [
{
title: "Create a contact in HubSpot.",
slug: "create-contact-in-hubspot",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/hubspot.ts",
},
],
},
{
identifier: "huggingface",
name: "Hugging Face",
examples: [
{
title: "Text classification with Hugging Face.",
slug: "text-classification-with-hugging-face",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/hugging-face.ts",
},
],
},
{
identifier: "infisical",
name: "Infisical",
},
{
identifier: "instagram",
name: "Instagram",
examples: [
{
title: "Post an image to Instagram",
slug: "post-image-to-instagram",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/instagram.ts",
},
],
},
{
identifier: "instabug",
name: "Instabug",
},
{
identifier: "keep",
name: "Keep",
},
{
identifier: "lemonsqueezy",
name: "Lemon Squeezy",
examples: [
{
title: "Get store information from Lemon Squeezy.",
slug: "get-store-information",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/lemon-squeezy.ts",
},
],
},
{
identifier: "linkedin",
name: "LinkedIn",
},
{
identifier: "linear",
name: "Linear",
examples: [
{
title: "Post Linear issues to Slack every weekday at 9am using Cron.",
slug: "daily-linear-issues-slack-alert",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearIssuesDailySlackAlert.ts",
},
{
title: "Create a Linear issue when a pull request is opened on a GitHub repo.",
slug: "linear-ticket-on-pr",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearCreateIssueOnPR.ts",
},
{
title: "Automatically comment and like any new Linear issues.",
slug: "automatically-comment-and-like-linear-issues",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearNewIssueReply.ts",
},
],
},
{
identifier: "loops",
name: "Loops",
examples: [
{
title: "Create a new contact in Loops.",
slug: "create-new-contact-in-loops",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/loops.ts",
},
],
},
{
identifier: "lotus",
name: "Lotus",
},
{
identifier: "mailchimp",
name: "Mailchimp",
},
{
identifier: "mailgun",
name: "Mailgun",
examples: [
{
title: "Send an email with Mailgun.",
slug: "send-email-with-mailgun",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/mailgun.ts",
},
],
},
{
identifier: "microsoftazure",
name: "Microsoft Azure",
},
{
identifier: "monday",
name: "Monday",
},
{
identifier: "mux",
name: "Mux",
},
{
identifier: "notion",
name: "Notion",
examples: [
{
title: "Retrieve a Notion page by ID.",
slug: "retrieve-notion-page",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/notion.ts",
},
],
},
{
identifier: "novu",
name: "Novu",
examples: [
{
title: "Create a new subscriber in Novu",
slug: "create-new-subscriber-in-novu",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/novu.ts",
},
],
},
{
identifier: "openai",
name: "OpenAI",
examples: [
{
title: "Summarize GitHub commits using OpenAI and then post them to Slack.",
slug: "openai-summarize-github-commits",
version: "1.0.0",
codeUrl:
"https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/summarizeGitHubCommits.ts",
},
{
title: "Generate a random joke using OpenAI.",
slug: "openai-generate-random-joke",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/jobs-showcase/raw/main/src/openAITellMeAJoke.ts",
},
{
title: "Generate an image from a prompt using OpenAI.",
slug: "openai-generate-image",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/openAIGenerateImage.ts",
},
],
},
{
identifier: "pagerduty",
name: "PagerDuty",
},
{
identifier: "plain",
name: "Plain",
examples: [
{
title: "Update or create customer information based on an identifier.",
slug: "plain-update-customer-information",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/plainUpdateCustomer.ts",
},
],
},
{
identifier: "posthog",
name: "Posthog",
},
{
identifier: "raycast",
name: "Raycast",
},
{
identifier: "reddit",
name: "Reddit",
},
{
identifier: "replicate",
name: "Replicate",
examples: [
{
title: "Generate a cinematic image with Replicate.",
slug: "generate-cinematic-image",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/replicateCinematicPrompt.ts",
},
],
},
{
identifier: "resend",
name: "Resend",
examples: [
{
title: "Send a drip email campaign over 30 days, triggered by an event.",
slug: "resend-send-drip-campaign",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/resendDripCampaign.tsx",
},
{
title: "Send an email built using React with Resend.",
slug: "send-react-email",
version: "1.0.0",
codeUrl:
"https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/resendSendReactEmail.tsx",
},
{
title: "Send a basic email with Resend.",
slug: "resend-send-basic-email",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/resendSendBasicEmail.ts",
},
],
},
{
identifier: "salesforce",
name: "Salesforce",
examples: [
{
title: "Create a new contact in Salesforce.",
slug: "salesforce-create-contact",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/salesforce.ts",
},
],
},
{
identifier: "segment",
name: "Segment",
examples: [
{
title: "Get source information from Segment.",
slug: "segment-get-source-information",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/segment.ts",
},
],
},
{
identifier: "sendgrid",
name: "SendGrid",
examples: [
{
title: "Send an activity summary email to users at 4pm every Friday.",
slug: "sendgrid-send-activity-summary",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/weeklyUserActivitySummary.ts",
},
{
title: "SendGrid send basic email.",
slug: "sendgrid-send-basic-email",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/sendGridSendBasicEmail.ts",
},
],
},
{
identifier: "shopify",
name: "Shopify",
examples: [
{
title: "Update a product variant price in Shopify.",
slug: "shopify-update-product-variant-price",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/shopify.ts",
},
],
},
{
identifier: "slack",
name: "Slack",
examples: [
{
title: "Posts Linear issues to Slack every weekday at 9am using Cron.",
slug: "slack-daily-linear-issues",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/linearIssuesDailySlackAlert.ts",
},
{
title: "Summarize GitHub commits using OpenAI and then post them to Slack.",
slug: "slack-openai-summarize-github-commits",
version: "1.0.0",
codeUrl:
"https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/summarizeGitHubCommits.ts",
},
{
title: "Send an activity summary email, and post it to Slack at 4pm every Friday.",
slug: "slack-sendgrid-send-activity-summary",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/weeklyUserActivitySummary.ts",
},
{
title: "Send a message to a Slack channel when a GitHub repo is starred.",
slug: "slack-post-github",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubNewStarToSlack.ts",
},
{
title:
"Send a reminder message to a Slack channel if a GitHub issue is left open for 24 hours.",
slug: "slack-github-issue-reminder",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/gitHubIssueReminder.ts",
},
],
},
{
identifier: "snyk",
name: "Snyk",
examples: [
{
title: "Get user details from Snyk.",
slug: "snyk-get-user-details",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/snyk.ts",
},
],
},
{
identifier: "spotify",
name: "Spotify",
},
{
identifier: "stabilityai",
name: "Stability AI",
examples: [
{
title: "Generate an image with Stability AI.",
slug: "stabilityai-generate-image",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/stability.ts",
},
],
},
{
identifier: "stripe",
name: "Stripe",
examples: [
{
title: "Update Supabase every time a Stripe account is updated.",
slug: "stripe-supabase-update",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/supabaseStripeUpdateDatabase.ts",
},
{
title: "Update Airtable when a new subscription is added to Stripe.",
slug: "stripe-sub-update-airtable",
version: "1.0.0",
codeUrl:
"https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/stripeNewSubscriptionUpdateAirtable.ts",
},
{
title: "Update Airtable database when there is a sale in Stripe.",
version: "1.0.0",
slug: "update-airtable-when-stripe-account-updated",
codeUrl:
"https://raw.githubusercontent.com/triggerdotdev/jobs-showcase/main/src/syncStripeWithAirtable.ts",
},
],
},
{
identifier: "supabase",
name: "Supabase",
examples: [
{
title: "Update Supabase every time a Stripe account is updated.",
slug: "stripe-supabase-update",
version: "1.0.0",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/supabaseStripeUpdateDatabase.ts",
},
],
},
{
identifier: "svix",
name: "Svix",
examples: [
{
title: "Create an application in Svix",
slug: "svix-create-application",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/svix.ts",
},
],
},
{
identifier: "todoist",
name: "Todoist",
examples: [
{
title: "Add a new project in Todoist.",
slug: "todoist-add-new-project",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/todoist.ts",
},
],
},
{
identifier: "trello",
name: "Trello",
},
{
identifier: "twilio",
name: "Twilio",
examples: [
{
title: "Send an SMS or WhatsApp message with Twilio",
slug: "twilio-send-sms-or-whatsapp-message",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/twilio.ts",
},
],
},
{
identifier: "typeform",
name: "Typeform",
examples: [
{
title: "Add a new record to Airtable when a Typeform response is submitted.",
version: "1.0.0",
slug: "new-airtable-record-from-typeform",
codeUrl:
"https://github.com/triggerdotdev/jobs-showcase/raw/main/src/typeformNewSubmissionUpdateAirtable.ts",
},
],
},
{
identifier: "whatsapp",
name: "WhatsApp",
examples: [
{
title: "Send a message to a WhatsApp number",
slug: "whatapp-send-message",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/whatsapp.ts",
},
],
},
{
identifier: "x",
name: "X (Twitter)",
examples: [
{
title: "Post a post to an X (Twitter) account",
slug: "post-to-x",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/x.ts",
},
],
},
{
identifier: "youtube",
name: "YouTube",
examples: [
{
title: "Search for a YouTube video",
slug: "youtube-search-video",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/youtube.ts",
},
],
},
{
identifier: "zbd",
name: "ZBD",
examples: [
{
title: "Send Satoshis to a ZBD account.",
slug: "zbd-send-satoshis",
version: "1.0.0",
codeUrl: "https://github.com/triggerdotdev/api-reference/raw/main/src/zbd.ts",
},
],
},
];
@@ -1,223 +0,0 @@
export type Api = {
identifier: string;
name: string;
};
export const apisList = [
{
identifier: "airtable",
name: "Airtable",
},
{
identifier: "algolia",
name: "Algolia",
},
{
identifier: "anthropic",
name: "Anthropic",
},
{
identifier: "appsmith",
name: "Appsmith",
},
{
identifier: "asana",
name: "Asana",
},
{
identifier: "aws",
name: "AWS",
},
{
identifier: "caldotcom",
name: "Cal.com",
},
{
identifier: "clickup",
name: "ClickUp",
},
{
identifier: "coda",
name: "Coda",
},
{
identifier: "discord",
name: "Discord",
},
{
identifier: "dropbox",
name: "Dropbox",
},
{
identifier: "facebook",
name: "Facebook",
},
{
identifier: "flickr",
name: "Flickr",
},
{
identifier: "github",
name: "GitHub",
},
{
identifier: "giphy",
name: "Giphy",
},
{
identifier: "gmail",
name: "Gmail",
},
{
identifier: "googlecalendar",
name: "Google Calendar",
},
{
identifier: "googlecloudplatform",
name: "Google Cloud Platform",
},
{
identifier: "googledocs",
name: "Google Docs",
},
{
identifier: "googledrive",
name: "Google Drive",
},
{
identifier: "googlemaps",
name: "Google Maps",
},
{
identifier: "hubspot",
name: "HubSpot",
},
{
identifier: "instagram",
name: "Instagram",
},
{
identifier: "infisical",
name: "Infisical",
},
{
identifier: "linkedin",
name: "LinkedIn",
},
{
identifier: "linear",
name: "Linear",
},
{
identifier: "loops",
name: "Loops",
},
{
identifier: "mailchimp",
name: "Mailchimp",
},
{
identifier: "mailgun",
name: "Mailgun",
},
{
identifier: "microsoftazure",
name: "Microsoft Azure",
},
{
identifier: "mux",
name: "Mux",
},
{
identifier: "notion",
name: "Notion",
},
{
identifier: "openai",
name: "OpenAI",
},
{
identifier: "plain",
name: "Plain",
},
{
identifier: "posthog",
name: "Posthog",
},
{
identifier: "reddit",
name: "Reddit",
},
{
identifier: "replicate",
name: "Replicate",
},
{
identifier: "resend",
name: "Resend",
},
{
identifier: "salesforce",
name: "Salesforce",
},
{
identifier: "segment",
name: "Segment",
},
{
identifier: "sendgrid",
name: "SendGrid",
},
{
identifier: "shopify",
name: "Shopify",
},
{
identifier: "slack",
name: "Slack",
},
{
identifier: "spotify",
name: "Spotify",
},
{
identifier: "stabilityai",
name: "Stability AI",
},
{
identifier: "stripe",
name: "Stripe",
},
{
identifier: "supabase",
name: "Supabase",
},
{
identifier: "svix",
name: "Svix",
},
{
identifier: "trello",
name: "Trello",
},
{
identifier: "twilio",
name: "Twilio",
},
{
identifier: "twitter",
name: "Twitter",
},
{
identifier: "typeform",
name: "Typeform",
},
{
identifier: "whatsapp",
name: "WhatsApp",
},
{
identifier: "youtube",
name: "YouTube",
},
];
@@ -80,7 +80,7 @@ export class HandleHttpEndpointService {
});
if (!httpEndpointEnvironment) {
logger.error("Could not find http endpoint environment", {
logger.debug("Could not find http endpoint environment", {
httpEndpointId: httpEndpoint.id,
environmentId: environment.id,
});
+25 -19
View File
@@ -1,26 +1,32 @@
import type { LogLevel } from "@trigger.dev/core";
import { Logger } from "@trigger.dev/core";
import { sensitiveDataReplacer } from "./sensitiveDataReplacer";
import { singleton } from "~/utils/singleton";
import { AsyncLocalStorage } from "async_hooks";
export const logger = singleton(
"logger",
() =>
new Logger(
"webapp",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString", "payload"],
sensitiveDataReplacer
)
const currentFieldsStore = new AsyncLocalStorage<Record<string, unknown>>();
export function trace<T>(fields: Record<string, unknown>, fn: () => T): T {
return currentFieldsStore.run(fields, fn);
}
export const logger = new Logger(
"webapp",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString", "payload"],
sensitiveDataReplacer,
() => {
const fields = currentFieldsStore.getStore();
return fields ? { ...fields } : {};
}
);
export const workerLogger = singleton(
"worker-logger",
() =>
new Logger(
"worker",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString"],
sensitiveDataReplacer
)
export const workerLogger = new Logger(
"worker",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString"],
sensitiveDataReplacer,
() => {
const fields = currentFieldsStore.getStore();
return fields ? { ...fields } : {};
}
);
@@ -1,6 +1,7 @@
import { RunNotification } from "@trigger.dev/core";
import { subtle } from "node:crypto";
import { PrismaClient, prisma } from "~/db.server";
import { EndpointApi } from "../endpointApi.server";
// Infer the type of the #findSubscription method
type FoundSubscription = NonNullable<
@@ -87,6 +88,34 @@ export class DeliverRunSubscriptionService {
);
}
return true;
}
case "ENDPOINT": {
const endpointId = subscription.recipient;
if (endpointId !== subscription.run.endpointId) {
return true;
}
const client = new EndpointApi(
subscription.run.environment.apiKey,
subscription.run.endpoint.url
);
const response = await client.deliverRunNotification(payload);
if (!response) {
throw new Error(
`Failed to deliver endpoint notification to ${subscription.run.endpoint.url}`
);
}
if (!response.ok) {
throw new Error(
`Failed to deliver endpoint notification to ${subscription.run.endpoint.url}: [${response.status}] ${response.statusText}`
);
}
return true;
}
}
@@ -107,6 +136,19 @@ export class DeliverRunSubscriptionService {
organization: true,
project: true,
event: true,
endpoint: true,
tasks: {
where: {
status: "ERRORED",
},
take: 1,
orderBy: {
startedAt: "desc",
},
include: {
attempts: true,
},
},
},
},
},
@@ -114,7 +156,20 @@ export class DeliverRunSubscriptionService {
}
#getPayload(run: FoundRun): RunNotification<any> {
const { id, job, version, statuses, environment, organization, project, event } = run;
const { id, job, version, statuses, environment, organization, project, event, tasks } = run;
const task = tasks[0]
? {
id: tasks[0].idempotencyKey,
cacheKey: tasks[0].displayKey,
status: tasks[0].status,
name: tasks[0].name,
icon: tasks[0].icon,
startedAt: tasks[0].startedAt,
error: tasks[0].output,
params: tasks[0].params,
}
: undefined;
const payload = {
id,
@@ -123,10 +178,11 @@ export class DeliverRunSubscriptionService {
startedAt: run.startedAt,
updatedAt: run.updatedAt,
completedAt: run.completedAt,
isTest: run.isTest,
executionDurationInMs: run.executionDuration,
executionCount: run.executionCount,
job: {
id: job.id,
id: job.slug,
version: version.version,
},
statuses: statuses.map((status) => ({
@@ -155,8 +211,14 @@ export class DeliverRunSubscriptionService {
id: event.id,
context: event.context,
timestamp: event.timestamp,
payload: event.payload,
},
...(run.status === "SUCCESS" ? { output: run.output } : { error: run.output }),
...(run.status === "SUCCESS"
? { output: run.output }
: {
error: run.output,
task,
}),
};
return payload as RunNotification<any>;
@@ -2,7 +2,6 @@ import {
ApiEventLog,
AutoYieldMetadata,
ConnectionAuth,
EndpointHeadersSchema,
RunJobAutoYieldWithCompletedTaskExecutionError,
RunJobBody,
RunJobError,
@@ -34,10 +33,11 @@ import { CompleteRunTaskService } from "~/routes/api.v1.runs.$runId.tasks.$id.co
import { formatError } from "~/utils/formatErrors.server";
import { safeJsonZodParse } from "~/utils/json";
import { EndpointApi } from "../endpointApi.server";
import { createExecutionEvent } from "../executions/createExecutionEvent.server";
import { logger } from "../logger.server";
import { forceYieldCoordinator } from "./forceYieldCoordinator.server";
import { workerQueue } from "../worker.server";
import { ResumeTaskService } from "../tasks/resumeTask.server";
import { workerQueue } from "../worker.server";
import { forceYieldCoordinator } from "./forceYieldCoordinator.server";
type FoundRun = NonNullable<Awaited<ReturnType<typeof findRun>>>;
type FoundTask = FoundRun["tasks"][number];
@@ -67,7 +67,7 @@ export class PerformRunExecutionV3Service {
this.#prismaClient = prismaClient;
}
public async call(input: PerformRunExecutionV3Input) {
public async call(input: PerformRunExecutionV3Input, driftInMs: number = 0) {
const run = await findRun(this.#prismaClient, input.id);
if (!run) {
@@ -80,7 +80,7 @@ export class PerformRunExecutionV3Service {
break;
}
case "EXECUTE_JOB": {
await this.#executeJob(run, input);
await this.#executeJob(run, input, driftInMs);
break;
}
}
@@ -177,7 +177,7 @@ export class PerformRunExecutionV3Service {
});
}
}
async #executeJob(run: FoundRun, input: PerformRunExecutionV3Input) {
async #executeJob(run: FoundRun, input: PerformRunExecutionV3Input, driftInMs: number = 0) {
try {
const { isRetry, resumeTaskId } = input;
@@ -275,9 +275,30 @@ export class PerformRunExecutionV3Service {
forceYieldCoordinator.registerRun(run.id);
const { response, parser, errorParser, durationInMs } = await client.executeJobRequest(
executionBody
);
await createExecutionEvent({
eventType: "start",
eventTime: new Date(),
drift: driftInMs,
organizationId: run.organizationId,
environmentId: run.environmentId,
projectId: run.projectId,
jobId: run.jobId,
runId: run.id,
});
const { response, parser, errorParser, headersParser, durationInMs } =
await client.executeJobRequest(executionBody);
await createExecutionEvent({
eventType: "finish",
eventTime: new Date(),
drift: 0,
organizationId: run.organizationId,
environmentId: run.environmentId,
projectId: run.projectId,
jobId: run.jobId,
runId: run.id,
});
forceYieldCoordinator.deregisterRun(run.id);
@@ -289,7 +310,7 @@ export class PerformRunExecutionV3Service {
// Update the endpoint version if it has changed
const rawHeaders = Object.fromEntries(response.headers.entries());
const headers = EndpointHeadersSchema.safeParse(rawHeaders);
const headers = headersParser.safeParse(rawHeaders);
if (
headers.success &&
@@ -306,6 +327,58 @@ export class PerformRunExecutionV3Service {
});
}
if (headers.success && headers.data["x-trigger-run-metadata"] && !run.internal) {
logger.debug("Endpoint responded with run metadata", {
metadata: headers.data["x-trigger-run-metadata"],
});
if (
headers.data["x-trigger-run-metadata"].successSubscription &&
!run.subscriptions.some((s) => s.event === "SUCCESS")
) {
await this.#prismaClient.jobRunSubscription.upsert({
where: {
runId_recipient_event: {
runId: run.id,
recipient: run.endpoint.id,
event: "SUCCESS",
},
},
create: {
runId: run.id,
recipient: run.endpoint.id,
recipientMethod: "ENDPOINT",
event: "SUCCESS",
status: "ACTIVE",
},
update: {},
});
}
if (
headers.data["x-trigger-run-metadata"].failedSubscription &&
!run.subscriptions.some((s) => s.event === "FAILURE")
) {
await this.#prismaClient.jobRunSubscription.upsert({
where: {
runId_recipient_event: {
runId: run.id,
recipient: run.endpoint.id,
event: "FAILURE",
},
},
create: {
runId: run.id,
recipient: run.endpoint.id,
recipientMethod: "ENDPOINT",
event: "FAILURE",
status: "ACTIVE",
},
update: {},
});
}
}
const rawBody = await response.text();
if (!response.ok) {
@@ -1220,6 +1293,7 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) {
status: true,
noop: true,
output: true,
outputIsUndefined: true,
parentId: true,
},
orderBy: {
@@ -1233,6 +1307,11 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) {
organization: true,
},
},
subscriptions: {
where: {
recipientMethod: "ENDPOINT",
},
},
_count: {
select: {
tasks: true,
@@ -1,20 +1,26 @@
import {
FetchOperationSchema,
FetchPollOperationSchema,
FetchRequestInit,
FetchRetryOptions,
FetchRetryStrategy,
RedactString,
RetryOptions,
calculateResetAt,
calculateRetryAt,
eventFilterMatches,
responseFilterMatches,
} from "@trigger.dev/core";
import { type Task } from "@trigger.dev/database";
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { formatUnknownError } from "~/utils/formatErrors.server";
import { safeJsonFromResponse } from "~/utils/json";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
import { taskOperationWorker, workerQueue } from "../worker.server";
import { ResumeTaskService } from "./resumeTask.server";
import { fetch } from "@whatwg-node/fetch";
import { fromZodError } from "zod-validation-error";
import { ulid } from "../ulid.server";
type FoundTask = Awaited<ReturnType<typeof findTask>>;
@@ -32,16 +38,166 @@ export class PerformTaskOperationService {
return;
}
if (task.status === "CANCELED") {
return;
}
if (task.status === "COMPLETED" || task.status === "ERRORED") {
return await this.#resumeRunExecution(task, this.#prismaClient);
}
if (!task.operation) {
return await this.#resumeTask(task, null, 0);
return await this.#resumeTask(task, null, null, 200, "fetch", 0);
}
switch (task.operation) {
case "fetch": {
case "fetch-poll": {
const pollOperation = FetchPollOperationSchema.safeParse(task.params);
if (!pollOperation.success) {
return await this.#resumeTaskWithError(
task,
fromZodError(pollOperation.error, {
prefix: "Invalid fetch poll params",
}).message
);
}
const { url, requestInit, timeout, interval, responseFilter, requestTimeout } =
pollOperation.data;
// check if we need to fail the task because it's timed out
const startedAt = task.startedAt;
if (!startedAt) {
return await this.#resumeTaskWithError(task, {
message: "Task has not been started",
});
}
if (Date.now() - startedAt.getTime() > timeout * 1000) {
return await this.#resumeTaskWithError(task, {
message: `Task timed out after ${timeout} seconds`,
});
}
const startTimeInMs = performance.now();
const abortController = new AbortController();
// calculate the actual timeout. If timeoutInMs is undefined, we use the default of 5s
// Also make sure the timeout is at least 1s, but not bigger than 5s
const actualTimeoutInMs = Math.min(
Math.max(requestTimeout?.durationInMs ?? 5000, 1000),
5000
);
const timeoutId = setTimeout(() => {
abortController.abort();
}, actualTimeoutInMs);
try {
logger.debug("PerformTaskOperationService.call poll request", {
task,
actualTimeoutInMs,
url,
responseFilter,
});
const startedAt = new Date();
const method = requestInit?.method ?? "GET";
const response = await fetch(url, {
method,
headers: normalizeHeaders(requestInit?.headers ?? {}),
body: requestInit?.body,
signal: abortController.signal,
});
clearTimeout(timeoutId);
const durationInMs = Math.floor(performance.now() - startTimeInMs);
const headers = Object.fromEntries(response.headers.entries());
logger.debug("PerformTaskOperationService.call poll response", {
url,
requestInit,
statusCode: response.status,
headers: Object.fromEntries(response.headers.entries()),
durationInMs,
});
const matchResult = await responseFilterMatches(response, responseFilter);
await this.#prismaClient.task.create({
data: {
id: ulid(),
idempotencyKey: ulid(),
runId: task.runId,
parentId: task.id,
name: "poll attempt",
icon: "activity",
status: "COMPLETED",
noop: true,
style: { style: "minimal", variant: "info" },
description: `${method} ${url} ${response.status}`,
params: {
status: response.status,
headers,
body: matchResult.body as any,
},
startedAt,
completedAt: new Date(),
},
});
if (matchResult.match) {
logger.debug("PerformTaskOperationService.call poll response matched", {
url,
matchResult,
});
return await this.#resumeTask(
task,
matchResult.body,
Object.fromEntries(response.headers.entries()),
response.status,
"fetch",
durationInMs
);
} else {
const retryAt = new Date(Date.now() + interval * 1000);
return await this.#retryTask(task, retryAt);
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
const durationInMs = Math.floor(performance.now() - startTimeInMs);
logger.debug("PerformTaskOperationService.call poll timed out", {
url,
durationInMs,
error,
});
const retryAt = this.#calculateRetryForTimeout(task, requestTimeout?.retry);
if (retryAt) {
return await this.#retryTask(task, retryAt);
}
return await this.#resumeTaskWithError(task, {
message: `Fetch timed out after ${actualTimeoutInMs.toFixed(0)}ms`,
});
}
throw error;
}
}
case "fetch":
case "fetch-response": {
const fetchOperation = FetchOperationSchema.safeParse(task.params);
if (!fetchOperation.success) {
@@ -97,7 +253,7 @@ export class PerformTaskOperationService {
});
if (!response.ok) {
const retryAt = this.#calculateRetryForResponse(task, retry, response);
const retryAt = this.#calculateRetryForResponse(task, retry, response, jsonBody);
if (retryAt) {
return await this.#retryTaskWithError(
@@ -117,7 +273,14 @@ export class PerformTaskOperationService {
}
}
return await this.#resumeTask(task, jsonBody, durationInMs);
return await this.#resumeTask(
task,
jsonBody,
Object.fromEntries(response.headers.entries()),
response.status,
task.operation,
durationInMs
);
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
const durationInMs = Math.floor(performance.now() - startTimeInMs);
@@ -157,13 +320,14 @@ export class PerformTaskOperationService {
#calculateRetryForResponse(
task: NonNullable<FoundTask>,
retry: FetchRetryOptions | undefined,
response: Response
response: Response,
body: any
): Date | undefined {
if (!retry) {
return;
}
const strategy = this.#getRetryStrategyForStatusCode(response.status, retry);
const strategy = this.#getRetryStrategyForResponse(response, body, retry);
if (!strategy) {
return;
@@ -180,11 +344,10 @@ export class PerformTaskOperationService {
return calculateRetryAt(strategy, task.attempts.length - 1);
}
case "headers": {
const remaining = response.headers.get(strategy.remainingHeader);
const resetAt = response.headers.get(strategy.resetHeader);
if (typeof remaining === "string" && typeof resetAt === "string" && remaining === "0") {
return new Date(Number(resetAt) * 1000 + addJitterInMs());
if (typeof resetAt === "string") {
return calculateResetAt(resetAt, strategy.resetFormat);
}
}
}
@@ -201,8 +364,9 @@ export class PerformTaskOperationService {
return calculateRetryAt(retry, task.attempts.length - 1);
}
#getRetryStrategyForStatusCode(
statusCode: number,
#getRetryStrategyForResponse(
response: Response,
body: any,
retry: FetchRetryOptions
): FetchRetryStrategy | undefined {
const statusCodes = Object.keys(retry);
@@ -211,7 +375,19 @@ export class PerformTaskOperationService {
const statusRange = statusCodes[i];
const strategy = retry[statusRange];
if (isStatusCodeInRange(statusCode, statusRange)) {
if (isStatusCodeInRange(response.status, statusRange)) {
if (strategy.bodyFilter) {
if (!body) {
continue;
}
if (eventFilterMatches(body, strategy.bodyFilter)) {
return strategy;
} else {
continue;
}
}
return strategy;
}
}
@@ -248,16 +424,26 @@ export class PerformTaskOperationService {
},
});
await workerQueue.enqueue(
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ tx, runAt: retryAt }
{ tx, runAt: retryAt, jobKey: `operation:${task.id}` }
);
});
}
async #retryTask(task: Task, retryAt: Date) {
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ runAt: retryAt, jobKey: `operation:${task.id}` }
);
}
async #resumeTaskWithError(task: NonNullable<FoundTask>, output: any) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.task.update({
@@ -284,7 +470,14 @@ export class PerformTaskOperationService {
});
}
async #resumeTask(task: NonNullable<FoundTask>, output: any, durationInMs: number) {
async #resumeTask(
task: NonNullable<FoundTask>,
output: any,
context: any,
status: number,
operation: "fetch" | "fetch-response",
durationInMs: number
) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.taskAttempt.updateMany({
where: {
@@ -296,12 +489,22 @@ export class PerformTaskOperationService {
},
});
const taskOutput =
operation === "fetch"
? output
: {
data: output,
headers: context,
status,
};
await tx.task.update({
where: { id: task.id },
data: {
status: "COMPLETED",
completedAt: new Date(),
output: output ? output : undefined,
output: taskOutput,
context: context ? context : undefined,
run: {
update: {
executionDuration: {
@@ -0,0 +1,220 @@
import { RunTaskBodyOutput, ServerTask } from "@trigger.dev/core";
import { TaskStatus } from "@trigger.dev/database";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { env } from "~/env.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import { generateSecret } from "~/services/sources/utils.server";
import { ulid } from "~/services/ulid.server";
import { taskOperationWorker, workerQueue } from "~/services/worker.server";
export class RunTaskService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
runId: string,
idempotencyKey: string,
taskBody: RunTaskBodyOutput
): Promise<ServerTask | undefined> {
const delayUntilInFuture = taskBody.delayUntil
? taskBody.delayUntil.getTime() > Date.now()
: false;
const callbackEnabled = taskBody.callback?.enabled ?? false;
// First
const existingTask = await this.#handleExistingTask(
runId,
idempotencyKey,
taskBody,
delayUntilInFuture,
callbackEnabled
);
if (existingTask) {
return taskWithAttemptsToServerTask(existingTask);
}
const run = await this.#prismaClient.jobRun.findUnique({
where: {
id: runId,
},
select: {
status: true,
forceYieldImmediately: true,
},
});
if (!run) throw new Error("Run not found");
const runConnection = taskBody.connectionKey
? await this.#prismaClient.runConnection.findUnique({
where: {
runId_key: {
runId,
key: taskBody.connectionKey,
},
},
select: {
id: true,
},
})
: undefined;
const results = await $transaction(this.#prismaClient, async (tx) => {
// If task.delayUntil is set and is in the future, we'll set the task's status to "WAITING", else set it to RUNNING
let status: TaskStatus;
if (run.status === "CANCELED") {
status = "CANCELED";
} else {
status =
delayUntilInFuture || callbackEnabled || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
}
const taskId = ulid();
const callbackUrl = callbackEnabled
? `${env.APP_ORIGIN}/api/v1/tasks/${taskId}/callback/${generateSecret(12)}`
: undefined;
const task = await tx.task.create({
data: {
id: taskId,
idempotencyKey,
displayKey: taskBody.displayKey,
runConnectionId: runConnection ? runConnection.id : undefined,
icon: taskBody.icon,
runId,
parentId: taskBody.parentId,
name: taskBody.name ?? "Task",
description: taskBody.description,
status,
startedAt: new Date(),
completedAt: status === "COMPLETED" || status === "CANCELED" ? new Date() : undefined,
noop: taskBody.noop,
delayUntil: taskBody.delayUntil,
params: taskBody.params ?? undefined,
properties: this.#filterProperties(taskBody.properties) ?? undefined,
redact: taskBody.redact ?? undefined,
operation: taskBody.operation,
callbackUrl,
style: taskBody.style ?? { style: "normal" },
childExecutionMode: taskBody.parallel ? "PARALLEL" : "SEQUENTIAL",
},
});
const taskAttempt = await tx.taskAttempt.create({
data: {
number: 1,
taskId: task.id,
status: "PENDING",
},
});
if (task.status === "RUNNING" && typeof taskBody.operation === "string") {
// We need to schedule the operation
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ tx, runAt: task.delayUntil ?? undefined, jobKey: `operation:${task.id}` }
);
} else if (task.status === "WAITING" && callbackUrl && taskBody.callback) {
if (taskBody.callback.timeoutInSeconds > 0) {
// We need to schedule the callback timeout
await workerQueue.enqueue(
"processCallbackTimeout",
{
id: task.id,
},
{
tx,
runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000),
jobKey: `process-callback:${task.id}`,
}
);
}
}
return { task, taskAttempt };
});
if (!results) {
return;
}
const { task, taskAttempt } = results;
return task
? taskWithAttemptsToServerTask({ ...task, attempts: [taskAttempt], run })
: undefined;
}
async #handleExistingTask(
runId: string,
idempotencyKey: string,
taskBody: RunTaskBodyOutput,
delayUntilInFuture: boolean,
callbackEnabled: boolean
) {
const existingTask = await this.#prismaClient.task.findUnique({
where: {
runId_idempotencyKey: {
runId,
idempotencyKey,
},
},
include: {
attempts: true,
run: true,
},
});
if (existingTask) {
if (existingTask.status === "CANCELED") {
const existingTaskStatus =
delayUntilInFuture || callbackEnabled || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
const resumedExistingTask = await this.#prismaClient.task.update({
where: {
id: existingTask.id,
},
data: {
status: existingTaskStatus,
startedAt: new Date(),
completedAt: existingTaskStatus === "COMPLETED" ? new Date() : undefined,
},
include: {
run: true,
attempts: true,
},
});
return resumedExistingTask;
}
return existingTask;
}
}
#filterProperties(properties: RunTaskBodyOutput["properties"]): RunTaskBodyOutput["properties"] {
if (!properties) return;
return properties.filter((property) => {
if (!property) return false;
return typeof property.label === "string" && typeof property.text === "string";
});
}
}
+85 -18
View File
@@ -24,6 +24,8 @@ import { ProbeEndpointService } from "./endpoints/probeEndpoint.server";
import { DeliverRunSubscriptionService } from "./runs/deliverRunSubscription.server";
import { DeliverRunSubscriptionsService } from "./runs/deliverRunSubscriptions.server";
import { ResumeTaskService } from "./tasks/resumeTask.server";
import { ExpireDispatcherService } from "./dispatchers/expireDispatcher.server";
import { InvokeEphemeralDispatcherService } from "./dispatchers/invokeEphemeralEventDispatcher.server";
const workerCatalog = {
indexEndpoint: z.object({
@@ -40,9 +42,6 @@ const workerCatalog = {
processCallbackTimeout: z.object({
id: z.string(),
}),
performTaskOperation: z.object({
id: z.string(),
}),
deliverHttpSourceRequest: z.object({ id: z.string() }),
refreshOAuthToken: z.object({
organizationId: z.string(),
@@ -93,6 +92,9 @@ const workerCatalog = {
resumeTask: z.object({
id: z.string(),
}),
expireDispatcher: z.object({
id: z.string(),
}),
};
const executionWorkerCatalog = {
@@ -108,12 +110,24 @@ const executionWorkerCatalog = {
}),
};
const taskOperationWorkerCatalog = {
performTaskOperation: z.object({
id: z.string(),
}),
invokeEphemeralDispatcher: z.object({
id: z.string(),
eventRecordId: z.string(),
}),
};
let workerQueue: ZodWorker<typeof workerCatalog>;
let executionWorker: ZodWorker<typeof executionWorkerCatalog>;
let taskOperationWorker: ZodWorker<typeof taskOperationWorkerCatalog>;
declare global {
var __worker__: ZodWorker<typeof workerCatalog>;
var __executionWorker__: ZodWorker<typeof executionWorkerCatalog>;
var __taskOperationWorker__: ZodWorker<typeof taskOperationWorkerCatalog>;
}
// this is needed because in development we don't want to restart
@@ -123,6 +137,7 @@ declare global {
if (env.NODE_ENV === "production") {
workerQueue = getWorkerQueue();
executionWorker = getExecutionWorkerQueue();
taskOperationWorker = getTaskOperationWorkerQueue();
} else {
if (!global.__worker__) {
global.__worker__ = getWorkerQueue();
@@ -134,6 +149,12 @@ if (env.NODE_ENV === "production") {
}
executionWorker = global.__executionWorker__;
if (!global.__taskOperationWorker__) {
global.__taskOperationWorker__ = getTaskOperationWorkerQueue();
}
taskOperationWorker = global.__taskOperationWorker__;
}
export async function init() {
@@ -148,6 +169,10 @@ export async function init() {
if (env.EXECUTION_WORKER_ENABLED === "true") {
await executionWorker.initialize();
}
if (env.TASK_OPERATION_WORKER_ENABLED === "true") {
await taskOperationWorker.initialize();
}
}
function getWorkerQueue() {
@@ -286,15 +311,6 @@ function getWorkerQueue() {
await service.call(payload.id);
},
},
performTaskOperation: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskOperationService();
await service.call(payload.id);
},
},
scheduleEmail: {
priority: 100,
maxAttempts: 3,
@@ -375,6 +391,15 @@ function getWorkerQueue() {
handler: async (payload, job) => {
const service = new ResumeTaskService();
return await service.call(payload.id);
},
},
expireDispatcher: {
priority: 10,
maxAttempts: 3,
handler: async (payload) => {
const service = new ExpireDispatcherService();
return await service.call(payload.id);
},
},
@@ -417,15 +442,57 @@ function getExecutionWorkerQueue() {
handler: async (payload, job) => {
const service = new PerformRunExecutionV3Service();
await service.call({
id: payload.id,
reason: payload.reason,
isRetry: false,
});
const driftInMs = Date.now() - job.run_at.getTime();
await service.call(
{
id: payload.id,
reason: payload.reason,
isRetry: false,
},
driftInMs
);
},
},
},
});
}
export { executionWorker, workerQueue };
function getTaskOperationWorkerQueue() {
return new ZodWorker({
name: "taskOperationWorker",
prisma,
runnerOptions: {
connectionString: env.DATABASE_URL,
concurrency: env.TASK_OPERATION_WORKER_CONCURRENCY,
pollInterval: env.TASK_OPERATION_WORKER_POLL_INTERVAL,
noPreparedStatements: env.DATABASE_URL !== env.DIRECT_URL,
schema: env.WORKER_SCHEMA,
maxPoolSize: env.TASK_OPERATION_WORKER_CONCURRENCY,
},
shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT,
schema: taskOperationWorkerCatalog,
tasks: {
performTaskOperation: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskOperationService();
await service.call(payload.id);
},
},
invokeEphemeralDispatcher: {
priority: 0, // smaller number = higher priority
maxAttempts: 10,
handler: async (payload, job) => {
const service = new InvokeEphemeralDispatcherService();
await service.call(payload.id, payload.eventRecordId);
},
},
},
});
}
export { executionWorker, workerQueue, taskOperationWorker };
+5
View File
@@ -369,3 +369,8 @@ export function docsIntegrationPath(api: string) {
export function docsCreateIntegration() {
return `${docsRoot()}/integrations/create`;
}
//api
export function apiReferencePath(apiSlug: string) {
return `https://trigger.dev/apis/${apiSlug}`;
}
@@ -1,4 +1,5 @@
import { eventStream } from "remix-utils/sse/server";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
type SseProps = {
@@ -17,20 +18,21 @@ type Event = {
};
export function sse({ request, pingInterval = 1000, updateInterval = 348, run }: SseProps) {
if (env.DISABLE_SSE === "1" || env.DISABLE_SSE === "true") {
return new Response("SSE disabled", { status: 200 });
}
let pinger: NodeJS.Timer | undefined = undefined;
let updater: NodeJS.Timer | undefined = undefined;
let timeout: NodeJS.Timeout | undefined = undefined;
const abort = () => {
if (pinger) {
clearInterval(pinger);
}
if (updater) {
clearInterval(updater);
}
clearInterval(pinger);
clearInterval(updater);
clearTimeout(timeout);
};
return eventStream(request.signal, (send) => {
return eventStream(request.signal, (send, close) => {
const safeSend = (args: { event?: string; data: string }) => {
try {
send(args);
@@ -53,18 +55,30 @@ export function sse({ request, pingInterval = 1000, updateInterval = 348, run }:
});
}
abort();
close();
}
};
pinger = setInterval(() => {
if (request.signal.aborted) {
return abort();
}
safeSend({ event: "ping", data: new Date().toISOString() });
}, pingInterval);
updater = setInterval(async () => {
updater = setInterval(() => {
if (request.signal.aborted) {
return abort();
}
run(safeSend, abort);
}, updateInterval);
timeout = setTimeout(() => {
close(); // close the connection after 1 minute of inactivity, which will refresh the connection (that's why we aren't using abort)
}, 60 * 1000); // 1 minute
return abort;
});
}
+4 -2
View File
@@ -29,6 +29,7 @@
"/public/build"
],
"dependencies": {
"@aws-sdk/client-sqs": "^3.445.0",
"@codemirror/autocomplete": "^6.3.1",
"@codemirror/commands": "^6.1.2",
"@codemirror/lang-javascript": "^6.1.1",
@@ -62,7 +63,7 @@
"@remix-run/v1-meta": "^0.1.3",
"@tabler/icons-react": "^2.39.0",
"@team-plain/typescript-sdk": "^2.2.0",
"@trigger.dev/companyicons": "^1.5.14",
"@trigger.dev/companyicons": "^1.5.32",
"@trigger.dev/core": "workspace:*",
"@trigger.dev/core-backend": "workspace:*",
"@trigger.dev/database": "workspace:*",
@@ -102,7 +103,7 @@
"react-use": "^17.4.0",
"recharts": "^2.8.0",
"remix-auth": "^3.6.0",
"remix-auth-email-link": "^2.1.0",
"remix-auth-email-link": "2.0.2",
"remix-auth-github": "^1.6.0",
"remix-typedjson": "0.3.1",
"remix-utils": "^7.1.0",
@@ -111,6 +112,7 @@
"simplur": "^3.0.1",
"slug": "^6.0.0",
"sonner": "^1.0.3",
"sqs-consumer": "^7.4.0",
"tailwind-merge": "^1.12.0",
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss-animate": "^1.0.5",
+3 -1
View File
@@ -20,7 +20,9 @@ app.use((req, res, next) => {
next();
});
app.use(compression());
if (process.env.DISABLE_COMPRESSION !== "1") {
app.use(compression());
}
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable("x-powered-by");
+1
View File
@@ -0,0 +1 @@
<Warning>This feature is in beta and has not yet been deployed to the Trigger.dev cloud</Warning>
+6
View File
@@ -36,6 +36,12 @@
<ParamField body="enabled" type="boolean">
The `enabled` property is an optional property that specifies whether the Job is enabled or not. The Job will be enabled by default if you omit this property. When a job is disabled, no new runs will be triggered or resumed. In progress runs will continue to run until they are finished or delayed by using `io.wait`.
</ParamField>
<ParamField body="onSuccess" type="function">
The `onSuccess` property is an optional property that specifies a callback function to run when the Job finishes successfully. The callback function receives a [Run Notification](/sdk/run-notification) object as it's only parameter.
</ParamField>
<ParamField body="onFailure" type="function">
The `onFailure` property is an optional property that specifies a callback function to run when the Job fails to complete successfully. The callback function receives a [Run Notification](/sdk/run-notification) object as it's only parameter.
</ParamField>
<ParamField body="logLevel" type="log | error | warn | info | debug">
The `logLevel` property is an optional property that specifies the level of
logging for the Job. The level is inherited from the client if you omit this property.
+52
View File
@@ -0,0 +1,52 @@
<ParamField body="events" type="array" required>
<Expandable title="event properties" defaultOpen>
<ParamField body="name" type="string" required>
The `name` property must exactly match any subscriptions you want to
trigger.
</ParamField>
<ParamField body="payload" type="any">
The `payload` property will be sent to any matching Jobs and will appear
as the `payload` param of the `run()` function. You can leave this
parameter out if you just want to trigger a Job without any input data.
</ParamField>
<ParamField body="context" type="any">
The optional `context` property will be sent to any matching Jobs and will
be passed through as the `context.event.context` param of the `run()`
function. This is optional but can be useful if you want to pass through
some additional context to the Job.
</ParamField>
<ParamField body="id" type="string">
The `id` property uniquely identify this particular event. If unset it
will be set automatically using `ulid`.
</ParamField>
<ParamField body="timestamp" type="Date">
This is optional, it defaults to the current timestamp. Usually you would
only set this if you have a timestamp that you wish to pass through, e.g.
you receive a timestamp from a service and you want the same timestamp to
be used in your Job.
</ParamField>
<ParamField body="source" type="string">
This is optional, it defaults to "trigger.dev". It can be useful to set
this as you can filter events using this in the `eventTrigger()`.
</ParamField>
</Expandable>
</ParamField>
<ParamField body="options" type="object">
<Expandable title="properties" defaultOpen>
<ParamField body="deliverAt" type="Date">
An optional Date when you want the event to Trigger Jobs. The event will
be sent to the platform immediately but won't be acted upon until the
specified time.
</ParamField>
<ParamField body="deliverAfter" type="number">
An optional number of seconds you want to wait for the event to Trigger
any relevant Jobs. The event will be sent to the platform immediately but
won't be acted upon until the specified time.
</ParamField>
<ParamField body="accountId" type="string">
This optional param will be used by the Trigger.dev Connect feature, which
is coming soon.
</ParamField>
</Expandable>
</ParamField>
+29
View File
@@ -0,0 +1,29 @@
<ResponseField name="events" type="array">
<Expandable title="properties" defaultOpen>
<ResponseField name="id" type="string" required>
The `id` of the event that was sent.
</ResponseField>
<ResponseField name="name" type="string" required>
The `name` of the event that was sent.
</ResponseField>
<ResponseField name="payload" type="any" required>
The `payload` of the event that was sent
</ResponseField>
<ResponseField name="timestamp" type="Date" required>
The `timestamp` of the event that was sent
</ResponseField>
<ResponseField name="context" type="any">
The `context` of the event that was sent. Is `undefined` if no context was
set when sending the event.
</ResponseField>
<ResponseField name="deliverAt" type="Date">
The timestamp when the event will be delivered to any matching Jobs. Is
`undefined` if `deliverAt` or `deliverAfter` wasn't set when sending the
event.
</ResponseField>
<ResponseField name="deliveredAt" type="Date">
The timestamp when the event was delivered. Is `undefined` if `deliverAt`
or `deliverAfter` were set when sending the event.
</ResponseField>
</Expandable>
</ResponseField>
@@ -3,13 +3,11 @@ title: HTTP endpoints
description: HTTP endpoints allow you to trigger your Jobs from any webhooks.
---
<Warning>This feature is in beta and not yet available to use on the Trigger.dev Cloud.</Warning>
Sometimes you want to subscribe to changes from an API, and we don't have [an Integration](/integrations/introduction) for it yet. That's when you can use `defineHttpEndpoint` to receive webhooks, verify them, and create an [HTTP Trigger](/documentation/concepts/triggers/http).
## Defining an HTTP endpoint
Defining an HTTP endpoint creates a URL and secret which you'll enter into Cal.com's website. It also attaches a `verify` function that is called when a webhook is received. It's compulsory to return a result from this function 90% of the time you can use our `verifyRequestSignature` helper function.
We'll use Cal.com as an example:
```ts
const caldotcom = client.defineHttpEndpoint({
@@ -35,6 +33,10 @@ const caldotcom = client.defineHttpEndpoint({
});
```
When this code runs (and you're running the CLI dev command) the HTTP endpoint will be created and be visible in the Trigger.dev dashboard.
The `verify` function is compulsory and is automatically called when a webhook is received. It's required to return a result from this function 90% of the time you can use our `verifyRequestSignature` helper function.
## Getting the URL and secret
In our dashboard, you can navigate to the HTTP endpoints page. From there you can select your endpoint and copy the URL (1) and secret (2) for the appropriate Environment.
@@ -4,8 +4,6 @@ sidebarTitle: "HTTP"
description: "HTTP Triggers allow you to trigger your Jobs from any webhooks."
---
<Warning>This feature is in beta and not yet available to use on the Trigger.dev Cloud.</Warning>
Sometimes you want to subscribe to changes from an API, and we don't have [an Integration](/integrations/introduction) for it yet. That's when you can use `defineHttpEndpoint` to receive webhooks, verify them, and create an HTTP Trigger.
You should read the [HTTP endpoint](/documentation/concepts/http-endpoints) documentation to understand how to create an HTTP endpoint.
@@ -4,8 +4,6 @@ sidebarTitle: "Manual Invoke"
description: "Invoke Jobs manually using the invoke Trigger"
---
<Warning>This feature is in beta and not yet available to use on the Trigger.dev Cloud.</Warning>
Sometimes it makes sense to be able to invoke a Job manually, without having to specify an event, especially for cases where you want to get notified when the invoked Job Run is complete.
To specify that a job is manually invokable, you can use the `invokeTrigger()` function when defining a job:
+255
View File
@@ -0,0 +1,255 @@
---
title: "Task Library"
description: "These are the built-in tasks that are available to use in your Jobs."
---
Welcome to the Trigger.dev Task Library 📚. You may be wondering, what is a Task and why are there a library of them? Well you see, Trigger.dev works by divvying up a long-running job execution into a bunch of little tasks, each one taking less time then a single serverless function execution. 💫
You can define and run your own tasks easily using [io.runTask()](/sdk/io/runtask), or you can use one of our [Integrations](/integrations/introduction) which are tasks for specific APIs, like OpenAI or Stripe.
<Note>Read more about how Tasks work [here](/documentation/concepts/tasks).</Note>
We also have a growing library of built-in tasks that you can use in your Jobs through the `io` object. These tasks are designed to be generic and reusable, and are a great way to get started with Trigger.dev.
<Info>
You may notice that I'm using emojis for all the cache keys below, which is totally 💯% fine as
long as they are unique inside a run. Read more about how cache keys work
[here](/documentation/concepts/tasks#task-cache-keys)
</Info>
## `wait`
This task allows you to resume executing your job after a certain amount of time has passed:
```ts
await io.wait("⏰", 60); // wait 60 seconds
```
Internally this task is considered a "noop", and noop tasks have no output.
[reference docs](/sdk/io/wait)
## `waitForRequest`
You supply this task with a callback to receive a URL. When a POST request is made to that URL, the JSON body of the request becomes the task output.
The example below uses `waitForRequest` to capture a Screenshot of a website using [ScreenshotOne.com](https://screenshotone.com/) and passes the callback URL to the webhook URL to get notified when the screenshot is finished:
```ts
const result = await io.waitForRequest<ScreenshotResponse>(
"📸",
async (url) => {
await fetch(`https://api.screenshotone.com/take`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
webhook_url: url, // this is the URL that will be called when the screenshot is ready
access_key: "my-access-key",
url: "https://trigger.dev",
store: "true",
storage_path: "my-screeshots",
response_type: "json",
async: "true",
storage_return_location: "true",
}),
});
},
{
timeoutInSeconds: 300, // wait up to 5 minutes for the screenshot to be ready
}
);
```
We actually originally built this task for our [Replicate integration](/integrations/apis/replicate), which accepts a callback URL to notify you when a prediction is ready. So this allows you to write very succinct code to create a prediction and wait for it's results:
```ts
const sdPrediction = await io.replicate.predictions.createAndAwait("🧑‍🎨", {
version: "ac732df83cea7fff18b8472768c88ad041fa750ff7682a21affe81863cbe77e4",
input: {
prompt: "What is the meaning of life?",
},
});
```
[reference docs](/sdk/io/wait-for-request)
## `waitForEvent`
This task allows you to wait for an event to be sent. To read about how events work, check out the [Events](/documentation/concepts/triggers/events) documentation.
```ts
const event = await io.waitForEvent(
"🥂",
{
name: "user.created",
schema: z.object({
id: z.string(),
createdAt: z.coerce.date(),
isAdmin: z.boolean(),
}),
filter: {
isAdmin: [true], // Only wait for events where isAdmin is true
},
},
{
timeoutInSeconds: 60 * 60, // Wait for up to an hour
}
);
```
The event object returned from this task is the full event object that was sent, including `id`, `name`, `payload`, `context`, and more.
[reference docs](/sdk/io/wait-for-event)
## `backgroundFetch`
This task allows you to perform a `fetch` request in the background, and then resume the execution of your job after the request has completed.
```ts
const body = io.backgroundFetch<MyResponseData>("🕸️", "https://example.com/api/endpoint", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: redactString`Bearer ${auth.apiKey}`,
},
body: JSON.stringify({ foo: "bar" }),
});
```
This is useful for when an API is slow to respond and might not finish before your serverless function times out. We created this task to power our [OpenAI integration](/integrations/apis/openai), which can sometimes take more than a minute to respond:
```ts
// This uses backgroundFetch under the hood
await io.openai.chat.completions.backgroundCreate("💬", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
[reference docs](/sdk/io/backgroundfetch)
## `backgroundPoll`
This task is similar to `backgroundFetch`, but instead of waiting for a single request to complete, it will poll a URL until it returns a certain value.
```ts
const result = await io.backgroundPoll<{ foo: string }>("🔃", {
url: "https://example.com/api/endpoint",
interval: 10, // every 10 seconds
timeout: 300, // stop polling after 5 minutes
responseFilter: {
// stop polling once this filter matches
status: [200],
body: {
status: ["SUCCESS"],
},
},
});
```
## `logger`
The logger object allows you to log messages to the Trigger.dev console. This is useful for debugging your jobs, or just to see what's going on inside your job.
```ts
await io.logger.info("This is an info message");
```
You can optionally pass a `context` object to the logger, which will be displayed in the console:
```ts
await io.logger.info("This is an info message", {
foo: "bar",
});
```
We support the following log levels:
- `io.logger.debug()`
- `io.logger.info()`
- `io.logger.warn()`
- `io.logger.error()`
<Note>
You may notice these tasks don't include cache keys. We automatically create a cache key for you
based on the message and the log-level
</Note>
[reference docs](/sdk/io/logger)
## `random`
Use this task to generate a random number that stays stable during run retries/resumes:
```ts
const randomNumber = await io.random("🎲", {
min: 1,
max: 100,
});
```
[reference docs](/sdk/io/random)
## `sendEvent`
This task allows you to send an event from inside your job run.
If you want to send an event from outside a run (e.g. just from your backend) you should use [client.sendEvent()](/sdk/triggerclient/instancemethods/sendevent) instead.
```ts
await io.sendEvent("🚚", {
id: "e_1234567890",
name: "new.user",
payload: {
userId: "u_1234567890",
},
});
```
[reference docs](/sdk/io/sendevent)
## `getEvent`
This task allows you to get an event by ID from inside your job run.
If you want to get an event from outside a run (e.g. just from your backend) you should use [client.getEvent()](/sdk/triggerclient/instancemethods/getevent) instead.
```ts
const event = await io.getEvent("📥", "e_1234567890");
```
[reference docs](/sdk/io/getevent)
## `cancelEvent`
If you send an event that has a delivery date in the future, you can use this task to cancel it.
```ts
await io.sendEvent(
"🚚",
{
id: "e_1234567890",
name: "new.user",
payload: {
userId: "u_1234567890",
},
},
{
deliverAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // deliver in 24 hours
}
);
// Later on, if you want to cancel the event:
await io.cancelEvent("🚫", "e_1234567890");
```
## `createStatus`
Coming soon
@@ -68,7 +68,7 @@ client.defineJob({
});
```
## 5. Add integrations
## 5. Add integrations (optional)
Integrations provide a convienent way to create and run tasks against authenticated APIs inside your Job's run function. You'll need to pass them in the `integrations` option when defining your Job.
@@ -165,6 +165,7 @@ client.defineJob({
trigger: eventTrigger({
name: "my.event",
}),
// Optional integration
integrations: { slack },
run: async (payload, io, context) => {
await io.logger.info("Received the my.event event", { payload });
@@ -198,6 +199,7 @@ client.defineJob({
trigger: eventTrigger({
name: "my.event",
}),
// Optional integration
integrations: { slack },
run: async (payload, io, context) => {
// Use runTask with the "get-user" cacheKey, and return the user
@@ -366,17 +368,15 @@ We recommend exploring all of the below sections to fully understand how to crea
</Card>
<Card title="Example Jobs" icon="slot-machine" href="/examples">
View example Jobs / the example jobs repo. These are a great starting
point for creating your own Jobs.
</Card>
<Card title="SDK reference" icon="code" href="/sdk">
How to use the SDK. This includes all the available Tasks, triggers and
actions you can use.
</Card>
<Card title="Integrations" icon="grid-2" href="/integrations">
Integrations make it easy to authenticate and use APIs.
Learn how to use and create integrations.
</Card>
<Card title="Integrations" icon="grid-2" href="/integrations">
Integrations make it easy to authenticate and use APIs. Learn how to use and create integrations.
</Card>
<Card title="API catalog with code samples" icon="code" href="https://trigger.dev/apis">
Find code examples for the API you need that you can copy and paste into your
projects.
</Card>
<Card title="SDK reference" icon="book-open" href="/sdk">
How to use the SDK. This includes all the available Tasks, triggers and actions you can use.
</Card>
</CardGroup>
+17 -11
View File
@@ -1,17 +1,23 @@
---
title: Example Jobs
description: "Jobs and projects you can use to get started."
description: "Jobs and code examples you can use to get started."
---
<Card
title="Browse our Jobs Showcase"
icon="rocket-launch"
href="https://trigger.dev/showcase"
color="#EC4899"
>
The showcase is our ever-growing library of Jobs. Use them as they are, right out of the box, or
customize them to suit your needs.
</Card>
<CardGroup>
<Card
title="Browse our Jobs Showcase"
icon="rocket-launch"
href="https://trigger.dev/showcase"
color="#EC4899"
>
The showcase is our library of Jobs. Use them as they are, right out of the box, or customize
them to suit your needs.
</Card>
<Card title="API catalog with code samples" icon="code" href="https://trigger.dev/apis">
Find code examples for the API you need that you can copy and paste into your
projects.
</Card>
</CardGroup>
### Full projects
@@ -24,6 +30,6 @@ To run them, simply follow the instructions in the README files linked below.
| [OpenAI text summarizer](https://github.com/triggerdotdev/examples/tree/main/openai-text-summarizer) | An app which uses OpenAI to summarize an article and then post the result to Slack. | [OpenAI](https://trigger.dev/docs/integrations/apis/openai) [Slack](https://trigger.dev/docs/integrations/apis/slack) | Trigger.dev | ✅ |
| [Supabase onboarding emails](https://github.com/triggerdotdev/examples/tree/main/supabase-onboarding-emails) | When a user signs up and confirms their email address, they will receive 3 "onboarding" emails over 2 days using Resend.com and Trigger.dev | [Supabase](https://trigger.dev/docs/integrations/apis/supabase) [Resend](https://trigger.dev/docs/integrations/apis/resend) | Trigger.dev | ✅ |
| [Generate presentation titles using OpenAI](https://github.com/triggerdotdev/examples/tree/main/express-vanilla) | Generate presentation titles using OpenAI background jobs with Node.js, Express and Trigger.dev | [OpenAI](https://trigger.dev/docs/integrations/apis/openai) | [lirantal](https://github.com/lirantal) | ✅ |
| [Send a basic email with Resend](https://github.com/triggerdotdev/examples/tree/main/resend-email-form) | Send a basic email from a form with Resend | [Resend](https://trigger.dev/docs/integrations/apis/resend) | Trigger.dev | ✅ |
| [Send a basic email with Resend](https://github.com/triggerdotdev/examples/tree/main/resend-email-form) | Send a basic email from a form with Resend | [Resend](https://trigger.dev/docs/integrations/apis/resend) | Trigger.dev | ✅ |
| AI landing page copy generator | Copies your site and generates new copy using OpenAI | [OpenAI](https://trigger.dev/docs/integrations/apis/openai) | Trigger.dev | 🛠️ |
| AI changelog generator | Generates a changelog from your GitHub commits using OpenAI | [OpenAI](https://trigger.dev/docs/integrations/apis/openai) [GitHub](https://trigger.dev/docs/integrations/apis/github) | Trigger.dev | 🛠️ |
+13 -9
View File
@@ -15,9 +15,8 @@ Gets multiple records for a table. You can filter. [Official Airtable Docs](http
```ts example.ts
// Gets multiple records from the table.
// Here we only get the Status fields (columns)
await table.getRecords("multiple records", {
fields: ["Status"],
fields: ["Status"], // Here we only get the Status fields (columns)
});
```
@@ -39,7 +38,6 @@ Create one or more records in a table. [Official Airtable Docs](https://airtable
await table.createRecords("create records", [
{
// Define the new record to be created
// with field values for "Launch goals" and "Status."
fields: {
"Launch goals": "Created from Trigger.dev",
Status: "In progress",
@@ -80,16 +78,22 @@ await table.deleteRecords(
);
```
### `runTask`
Do anything thats possible with the official Airtable Node SDK.
## Example Usage
You can optionally add the types for your Table this gives you nice type inference and errors when writing your code.
In this demo we will use the Airtable to get records from a table, create a new record, update it and then delete it.
```ts
// This is the type definition for my Table
// Importing necessary modules and packages.
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import { Airtable } from "@trigger.dev/airtable";
import { z } from "zod";
// Creating an instance of the Airtable client with personal access token obtained from an environment variable.
const airtable = new Airtable({
id: "airtable",
apiKey: process.env.AIRTABLE_PERSONAL_ACCESS_TOKEN!,
});
type LaunchGoalsAndOkRs = {
"Launch goals"?: string;
DRI?: Collaborator;
+7 -12
View File
@@ -7,20 +7,17 @@ sidebarTitle: Overview & authentication
Our Airtable integration allows you to easily connect to the Airtable API and perform tasks such as creating / updating / deleting single or multiple records in your tables.
For examples of some of the things you can do with it, check out our Jobs Showcase:
<Card
title="Jobs Showcase - Airtable"
icon="rocket"
href="https://trigger.dev/showcase?tags=&integrations=airtable"
href="https://trigger.dev/showcase?tags=&apis=airtable"
>
Check out pre-built Airtable jobs in our showcase.
</Card>
## Installation
To get started with the Airtable integration on Trigger.dev, you need to install the `@trigger.dev/airtable` package.
You can do this using npm, pnpm, or yarn:
To get started with our Airtable integration, you need to install the `@trigger.dev/airtable` packages. You can do this using `npm`, `pnpm`, or `yarn`:
<CodeGroup>
@@ -44,7 +41,7 @@ To use the Airtable API with Trigger.dev, you can either use OAuth or a Personal
### OAuth
To use OAuth you can connect to Airtable via the Trigger.dev [web app](https://cloud.trigger.dev). Click 'Integrations' in the side panel of any project, and configure Airtable with the ID you want to use in your job and the required [scopes](https://airtable.com/developers/web/api/scopes).
To use OAuth you can connect to Airtable via the Trigger.dev [web app](https://cloud.trigger.dev). Click 'Integrations' in the side panel of any project, configure Airtable with a unique ID and the required [scopes](https://airtable.com/developers/web/api/scopes).
```ts
import { Airtable } from "@trigger.dev/airtable";
@@ -57,7 +54,7 @@ const airtable = new Airtable({
### Personal Access Token
You can create an Airtable Personal Access Token [here on their developer site](https://airtable.com/create/tokens).
You can create an Airtable Personal Access Token [here](https://airtable.com/create/tokens).
```ts
import { Airtable } from "@trigger.dev/airtable";
@@ -73,8 +70,6 @@ const airtable = new Airtable({
Once you have set up a Airtable client, you can use it to create tasks.
<CardGroup>
<Card title="Tasks" icon="sparkles" href="/integrations/apis/airtable-tasks">
Perform tasks such as creating / updating / deleting single or multiple records in table.
</Card>
</CardGroup>
<Card title="Tasks" icon="sparkles" href="/integrations/apis/airtable-tasks">
Perform tasks such as creating / updating / deleting single or multiple records in table.
</Card>
+4 -6
View File
@@ -5,22 +5,20 @@ sidebarTitle: Overview & authentication
## Overview
Our GitHub integration allows you to create triggers and tasks that interact with GitHub.
Trigger jobs when events happen, such as when a new issue is added to a repo, a commit is pushed, or a pull request is opened. You can also use the integration to perform tasks such as creating issues, getting information about a repo, adding comments, and much more.
For examples of some of the things you can do with it, check out our Jobs Showcase:
Our GitHub integration allows you to create triggers and tasks that interact with GitHub. Trigger jobs when events happen, like when a new issue is added to a repo, or when a pull request is opened, etc. You can also perform tasks like creating issues, getting information about a repo, adding comments, and much more.
<Card
title="Jobs Showcase - GitHub"
icon="rocket"
href="https://trigger.dev/showcase?tags=&integrations=github"
href="https://trigger.dev/showcase?tags=&apis=github"
>
Check out pre-built GitHub jobs in our showcase.
</Card>
## Installing the GitHub packages
To get started with our GitHub integration, you need to install the `@trigger.dev/github` packages. You can do this using `npm`, `pnpm`, or `yarn`:
<CodeGroup>
```bash npm
-643
View File
@@ -1,643 +0,0 @@
---
title: OpenAI tasks
sidebarTitle: Tasks
---
Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want.
---
## All tasks
### `createCompletion`
Generates text completions as per given prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat)
```ts example.ts
run: async (payload, io, ctx) => {
// This code demonstrates using OpenAI's text completion with the "davinci" model.
// It generates text based on the given prompt.
await io.openai.createCompletion("completion", {
model: "davinci",
prompt: "Once upon a time",
});
},
```
### `backgroundCreateCompletion`
Generates text completions in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat)
```ts example.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
```
You can also pass an optional third parameter to `backgroundCreateCompletion` to specify OpenAI request options:
```ts requestOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
}, {
headers: {
"User-Agent": "my-user-agent"
}
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
```
This task is implemented using [io.backgroundFetch()](/sdk/io/backgroundfetch) and so you can also pass a 4th parameter customizing the retry and timeout options:
```ts fetchOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
}, {
headers: {
"User-Agent": "my-user-agent"
}
}, {
timeout: {
durationInMs: 10000,
retry: {
limit: 3,
minTimeoutInMs: 1000,
factor: 2,
}
}
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
```
### `createChatCompletion`
Generates text completions in a conversational context. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/create)
```ts example.ts
run: async (payload, io, ctx) => {
// This code demonstrates chat completion with the "gpt-3.5-turbo" model.
// It simulates a conversation by providing messages and receiving a chat response.
await io.openai.createChatCompletion("chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
},
```
### `backgroundCreateChatCompletion`
Generates text completions in a conversational context in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/object)
```ts example.ts
run: async (payload, io, ctx) => {
// This code showcases background chat completion using the "gpt-3.5-turbo" model.
// It simulates a conversation with a user message and logs the response choices.
const response = await io.openai.backgroundCreateChatCompletion("background-chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
await io.logger.info("choices", response.choices);
},
```
You can also use the more "fluent" pattern used by the OpenAI SDK:
```ts fluent.ts
run: async (payload, io, ctx) => {
// This code showcases background chat completion using the "gpt-3.5-turbo" model.
// It simulates a conversation with a user message and logs the response choices.
const response = await io.openai.chat.completions.backgroundCreate("background-chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
await io.logger.info("choices", response.choices);
},
```
Additionally, you can pass an optional third parameter to specify OpenAI request options:
```ts requestOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.chat.completions.backgroundCreate("background-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
}, {
headers: {
"User-Agent": "my-user-agent"
}
});
await io.logger.info("choices", response.choices);
},
```
This task is implemented using [io.backgroundFetch()](/sdk/io/backgroundfetch) and so you can also pass a 4th parameter customizing the retry and timeout options:
```ts fetchOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.chat.completions.backgroundCreate("background-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
}, {
headers: {
"User-Agent": "my-user-agent"
}
}, {
timeout: {
durationInMs: 10000,
retry: {
limit: 3,
minTimeoutInMs: 1000,
factor: 2,
}
}
});
await io.logger.info("choices", response.choices);
},
```
### `retrieveModel`
Retrieves a specific model by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/retrieve)
```ts example.ts
run: async (payload, io, ctx) => {
// In this code snippet, we retrieve detailed information about a specific OpenAI model.
// Specify the ID of the model you want to retrieve. Replace 'your_model_id' with the actual model ID.
const modelIdToRetrieve = "your_model_id";
try {
// Retrieve the model information using the OpenAI API
const retrievedModel = await io.openai.retrieveModel("get-model", {
model: modelIdToRetrieve,
});
// Log the detailed model information
await io.logger.info("retrievedModel", retrievedModel);
} catch (error) {
// Handle errors, such as if the model with the provided ID does not exist.
await io.logger.error("Error retrieving model:", error.message);
}
},
```
### `listModels`
Lists the available models. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/list)
```ts example.ts
run: async (payload, io, ctx) => {
// This code lists available models without retrieving detailed information.
const models = await io.openai.listModels("list-models");
},
```
### `createEdit`
Edits a given text prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/edits/create)
```ts example.ts
run: async (payload, io, ctx) => {
// This code snippet demonstrates using the OpenAI API to create an edit task.
// Specify the task parameters:
const editTaskParams = {
model: "text-davinci-edit-001", // Replace with the desired model
input: "Thsi is ridddled with erors", // Replace with the input text
instruction: "Fix the spelling errors", // Replace with the editing instruction
};
try {
// Create an edit task using the OpenAI API
const editResponse = await io.openai.createEdit("edit", editTaskParams);
// Log the response
await io.logger.info("editResponse", editResponse);
} catch (error) {
// Handle any potential errors that may occur during the API request.
await io.logger.error("Error creating edit task:", error.message);
}
},
```
### `createImage`
Generates images from textual descriptions. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/create)
```ts example.ts
run: async (payload, io, ctx) => {
const imageResults = await io.openai.createImage("image", {
prompt: "A hedgehog wearing a party hat",
n: 2,
size: "256x256",
response_format: "url",
});
```
### `createImageEdit`
Creates an edited or extended image given an original image and a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createEdit)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for the image edit
const imageEditParams = {
style: "data:image/png;base64,base64_encoded_style_image",
content: "data:image/png;base64,base64_encoded_content_image",
};
// Create the image edit using the OpenAI API
const imageEditResponse = await io.openai.createImageEdit(imageEditParams);
// Log the response
await io.logger.info("imageEditResponse", imageEditResponse);
},
```
### `createImageVariation`
Creates a variation of a given image. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createVariation)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating an image variation
const imageVariationParams = {
image: "data:image/png;base64,base64_encoded_image",
variation: "brightness(1.2) contrast(0.8) rotate(45deg)",
};
// Create the image variation using the OpenAI API
const imageVariationResponse = await io.openai.createImageVariation(imageVariationParams);
// Log the response
await io.logger.info("imageVariationResponse", imageVariationResponse);
},
```
### `createEmbedding`
Generates embeddings for a given text. [Official OpenAI Docs](hhttps://platform.openai.com/docs/api-reference/embeddings/object)
```ts example.ts
run: async (payload, io, ctx) => {
// This code snippet demonstrates using the OpenAI API to create a text embedding.
// Specify the task parameters:
const embeddingTaskParams = {
model: "text-embedding-ada-002", // Replace with the desired model
input: "The food was delicious and the waiter...", // Replace with the input text
};
try {
// Create a text embedding using the OpenAI API
const embeddingResponse = await io.openai.createEmbedding("embedding", embeddingTaskParams);
// Log the response
await io.logger.info("embeddingResponse", embeddingResponse);
} catch (error) {
// Handle any potential errors that may occur during the API request.
await io.logger.error("Error creating text embedding:", error.message);
}
},
```
### `createFile`
Uploads a file to the OpenAI API. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/object)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a file
const fileParams = {
name: "example.txt",
content: "This is the content of the file.",
};
// Create the file using the OpenAI API
const fileResponse = await io.openai.createFile(fileParams);
// Log the response
await io.logger.info("fileResponse", fileResponse);
},
```
### `listFiles`
Lists the uploaded files. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/list)
```ts example.ts
run: async (payload, io, ctx) => {
// List the files available in your OpenAI account
const fileListResponse = await io.openai.listFiles();
// Log the list of files
await io.logger.info("fileListResponse", fileListResponse);
},
```
### `createFineTuneFile`
Uploads a file for fine-tuning a model. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a fine-tune file
const fineTuneFileParams = {
model: "text-davinci-002",
prompt: "Translate English to French: 'Hello, world.'",
language: "en",
description: "Fine-tune file for translation task",
};
// Create the fine-tune file using the OpenAI API
const fineTuneFileResponse = await io.openai.createFineTuneFile(fineTuneFileParams);
// Log the response
await io.logger.info("fineTuneFileResponse", fineTuneFileResponse);
},
```
### `createFineTune`
Fine-tunes a model on a given task. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/create)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a fine-tune task
const fineTuneParams = {
model: "text-davinci-002",
dataset: "your_dataset_id",
description: "Fine-tune task for custom dataset",
};
// Create the fine-tune task using the OpenAI API
const fineTuneResponse = await io.openai.createFineTune(fineTuneParams);
// Log the response
await io.logger.info("fineTuneResponse", fineTuneResponse);
},
```
### `listFineTunes`
Lists the available fine-tunes. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list)
```ts example.ts
run: async (payload, io, ctx) => {
// List the fine-tunes available in your OpenAI account
const fineTunesListResponse = await io.openai.listFineTunes();
// Log the list of fine-tunes
await io.logger.info("fineTunesListResponse", fineTunesListResponse);
},
```
### `retrieveFineTune`
Retrieves a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tune you want to retrieve
const fineTuneId = "your_fine_tune_id"; // Replace with the actual fine-tune ID
// Retrieve the fine-tune using the OpenAI API
const retrievedFineTune = await io.openai.retrieveFineTune(fineTuneId);
// Log the retrieved fine-tune
await io.logger.info("retrievedFineTune", retrievedFineTune);
},
```
### `cancelFineTune`
Cancels a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tune you want to cancel
const fineTuneIdToCancel = "your_fine_tune_id"; // Replace with the actual fine-tune ID
// Cancel the specified fine-tune using the OpenAI API
const cancellationResponse = await io.openai.cancelFineTune(fineTuneIdToCancel);
// Log the cancellation response
await io.logger.info("cancellationResponse", cancellationResponse);
},
```
### `createFineTuningJob`
Creates a job that fine-tunes a specified model from a given dataset. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/create)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a fine-tuning job
const fineTuningJobParams = {
fineTuneId: "your_fine_tune_id", // Replace with the actual fine-tune ID
datasetId: "your_dataset_id", // Replace with the ID of your dataset
model: "text-davinci-002", // Replace with the model for fine-tuning
n_examples: 100, // Replace with the number of examples
};
// Create the fine-tuning job using the OpenAI API
const fineTuningJobResponse = await io.openai.createFineTuningJob(fineTuningJobParams);
// Log the response
await io.logger.info("fineTuningJobResponse", fineTuningJobResponse);
},
```
### `retrieveFineTuningJob`
Get info about a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tuning job you want to retrieve
const fineTuningJobId = "your_fine_tuning_job_id"; // Replace with the actual job ID
// Retrieve the fine-tuning job using the OpenAI API
const retrievedJob = await io.openai.retrieveFineTuningJob(fineTuningJobId);
// Log the retrieved job
await io.logger.info("retrievedJob", retrievedJob);
},
```
### `cancelFineTuningJob`
Cancel a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tuning job you want to cancel
const fineTuningJobIdToCancel = "your_fine_tuning_job_id"; // Replace with the actual job ID
// Cancel the specified fine-tuning job using the OpenAI API
const cancellationResponse = await io.openai.cancelFineTuningJob(fineTuningJobIdToCancel);
// Log the cancellation response
await io.logger.info("cancellationResponse", cancellationResponse);
},
```
### `listFineTuningJobEvents`
List events for a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list-events)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tuning job for which you want to list events
const fineTuningJobId = "your_fine_tuning_job_id"; // Replace with the actual job ID
// List events for the specified fine-tuning job using the OpenAI API
const eventsListResponse = await io.openai.listFineTuningJobEvents(fineTuningJobId);
// Log the list of events
await io.logger.info("eventsListResponse", eventsListResponse);
},
```
### `listFineTuningJobs`
List fine tuning jobs. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list)
```ts example.ts
run: async (payload, io, ctx) => {
// List the fine-tuning jobs available in your OpenAI account
const jobsListResponse = await io.openai.listFineTuningJobs();
// Log the list of fine-tuning jobs
await io.logger.info("jobsListResponse", jobsListResponse);
},
```
## Example usage
In this example we'll create a task that generates a random joke using OpenAI GPT 3.5 .
```ts example.ts
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import { OpenAI } from "@trigger.dev/openai";
import { z } from "zod";
// Initialize a TriggerClient with the ID "jobs-showcase"
const client = new TriggerClient({ id: "jobs-showcase" });
// Create an instance of the OpenAI client and provide the OpenAI API key from environment variables
const openai = new OpenAI({
id: "openai",
apiKey: process.env.OPENAI_API_KEY!, // Replace with your actual OpenAI API key
});
// Define a job that uses OpenAI GPT-3.5 Turbo to tell jokes
client.defineJob({
id: "openai-tell-me-a-joke",
name: "OpenAI: tell me a joke",
version: "1.0.0",
trigger: eventTrigger({
name: "openai.tasks", // Define the trigger event name
schema: z.object({
jokePrompt: z.string(), // Expect a joke prompt as input
}),
}),
integrations: {
openai, // Use the OpenAI integration for this job
},
run: async (payload, io, ctx) => {
// Retrieve information about the GPT-3.5 Turbo model
await io.openai.retrieveModel("get-model", {
model: "gpt-3.5-turbo",
});
// List available models (optional, for reference)
const models = await io.openai.listModels("list-models");
// Generate a joke in the background using the chat conversation format
const jokeResult = await io.openai.backgroundCreateChatCompletion(
"background-chat-completion",
{
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: payload.jokePrompt, // User-provided joke prompt
},
],
}
);
// Return the generated joke as the result
return {
joke: jokeResult.choices[0]?.message?.content,
};
},
});
// These lines are specific to the Express framework and can be removed if not needed
import { createExpressServer } from "@trigger.dev/express";
createExpressServer(client);
```
+64 -4
View File
@@ -10,7 +10,7 @@ Our OpenAI integration allows you to easily perform AI-powered tasks, such as su
<Card
title="Jobs Showcase - OpenAI"
icon="rocket"
href="https://trigger.dev/showcase?tags=&integrations=openai"
href="https://trigger.dev/showcase?tags=&apis=openai"
>
Check out pre-built OpenAI jobs in our showcase.
</Card>
@@ -49,11 +49,71 @@ const openai = new OpenAI({
## Tasks
Once you have set up a OpenAI client, you can use it to create tasks.
Once you have set up a OpenAI client, you can add it to your job and start using the provided tasks:
```ts
client.defineJob({
id: "openai-job",
name: "OpenAI Job",
version: "1.0.0",
trigger: invokeTrigger(),
integrations: {
openai, // Add the OpenAI client as an integration
},
run: async (payload, io, ctx) => {
// Now you can access it through the io object
const completion = await io.openai.chat.completions.create("completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
},
});
```
As you can see above, we've replicated the API of the [OpenAI TypeScript SDK](https://github.com/openai/openai-node), with a crucial difference of adding the [Task Cache Key](https://trigger.dev/docs/documentation/concepts/tasks#task-cache-keys) as the first parameter.
We've also added a few convenience methods to make it easier to work with the OpenAI API, especially in a serverless environment. For example, you can run a Chat Completion task in the background with [backgroundCreate()](/integrations/apis/openai/chat#completions-backgroundcreate):
```ts
const completion = await io.openai.chat.completions.backgroundCreate("completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
See our full task reference below:
<CardGroup>
<Card title="Tasks" icon="sparkles" href="/integrations/apis/openai-tasks">
Perform different AI-powered tasks using OpenAI.
<Card title="Chat Completions" icon="sparkles" href="/integrations/apis/openai/chat">
Given a list of messages comprising a conversation, the model will return a response
</Card>
<Card title="Assistants (Beta)" icon="arrows-spin" href="/integrations/apis/openai/assistants">
Build assistants that can call models and use tools to perform tasks
</Card>
<Card title="Files" icon="file" href="/integrations/apis/openai/files">
Upload files to use with assistants and fine-tuning
</Card>
<Card title="Images" icon="image" href="/integrations/apis/openai/images">
Given a prompt and/or an input image, the model will generate a new image
</Card>
<Card title="Fine Tuning Jobs" icon="vial" href="/integrations/apis/openai/fine-tunes">
Manage fine-tuning jobs to tailor a model to your specific training data
</Card>
<Card title="Models" icon="server" href="/integrations/apis/openai/models">
List and describe the various models available in the API
</Card>
<Card title="Completions (Legacy)" icon="scroll" href="/integrations/apis/openai/completions">
Given a prompt, the model will return one or more predicted completions.
</Card>
</CardGroup>
@@ -0,0 +1,287 @@
---
title: Assistant Tasks
sidebarTitle: Assitants (Beta)
---
<Note>
This feature is currently marked as a "Beta" by OpenAI. Make sure to check our their [How
Assistants Work](https://platform.openai.com/docs/assistants/how-it-works) and [Assistants
Overview](https://platform.openai.com/docs/assistants/overview) guides.
</Note>
## Assistants
Build assistants that can call models and use tools to perform tasks. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/assistants)
### `create()`
Create an assistant with a model and instructions. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/assistants/createAssistant)
```ts example.ts
const file = await io.openai.files.createAndWaitForProcessing("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
const assistant = await io.openai.beta.assistants.create("create-assistant", {
name: "Data visualizer",
description:
"You are great at creating beautiful data visualizations. You analyze data present in .csv files, understand trends, and come up with data visualizations relevant to those trends. You also share a brief text summary of the trends observed.",
model: "gpt-4-1106-preview",
tools: [{ type: "code_interpreter" }],
file_ids: [file.id],
});
```
## Threads
Create threads that assistants can interact with. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/threads/createThread)
### `create()`
Create a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/assistants/createAssistant)
```ts example.ts
const thread = await io.openai.beta.threads.create("create-thread", {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
},
],
});
```
### `createAndRun()`
Create a thread and run it in one task.
```ts example.ts
const run = await io.openai.beta.threads.createAndRun("create-and-run-thread", {
assistant_id: "asst_abc123",
thread: {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
},
],
},
});
```
### `createAndRunUntilCompletion()`
Create a thread and runs it in one task, and only returns when the run is completed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const run = await io.openai.beta.threads.createAndRunUntilCompletion("create-thread", {
assistant_id: "asst_abc123",
thread: {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
},
],
},
});
if (run.status !== "completed") {
throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`);
}
// List all messages in the thread
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
```
### `retrieve()`
Retrieves a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/threads/getThread)
```ts example.ts
const thread = await io.openai.beta.threads.retrieve("get-thread", "thread_abc123");
```
### `update()`
Modifies a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/threads/modifyThread)
```ts example.ts
await io.openai.beta.threads.update("update-thread", "thread_abc123", {
metadata: {
foo: "bar",
},
});
```
### `del()`
Deletes a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/threads/deleteThread)
```ts example.ts
const deletedThread = await io.openai.beta.threads.del("update-thread", "thread_abc123");
```
## Messages
Create messages within threads. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/messages)
### `list()`
List all messages in a thread.
```ts example.ts
const messages = await io.openai.beta.threads.messages.list("list-messages", "thread_abc123");
```
### `create()`
Create a message. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/messages/createMessage)
```ts example.ts
const thread = await io.openai.beta.threads.create("get-thread");
const message = await io.openai.beta.threads.messages.create("create-message", thread.id, {
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
});
```
### `retrieve()`
Retrieve a message. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/messages/getMessage)
```ts example.ts
const message = await io.openai.beta.threads.messages.retrieve(
"get-message",
"thread_abc123",
"message_abc123"
);
```
### `update()`
Update a message. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/messages/modifyMessage)
```ts example.ts
await io.openai.beta.threads.messages.update("update-message", thread.id, message.id, {
metadata: {
foo: "bar",
},
});
```
## Runs
Represents an execution run on a thread. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/runs)
### `list()`
List all runs belonging to a thread.
```ts example.ts
const runs = await io.openai.beta.threads.runs.list("list-runs", "thread_abc123");
```
### `create()`
Create a run. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/createRun)
```ts example.ts
const run = await io.openai.beta.threads.runs.create("create-run", "thread_abc123", {
assistant_id: payload.id,
});
```
### `createAndWaitForCompletion()`
Create a run and only return when the run is completed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const run = await io.openai.beta.threads.runs.createAndWaitForCompletion(
"create-run",
"thread_abc123",
{
assistant_id: payload.id,
}
);
if (run.status !== "completed") {
throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`);
}
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
```
### `waitForCompletion()`
Wait for a run to complete by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const run = await io.openai.beta.threads.runs.create("create-run", "thread_abc123", {
assistant_id: payload.id,
});
const completedRun = await io.openai.beta.threads.runs.waitForCompletion(
"wait-for-completion",
"thread_abc123",
run.id
);
if (completedRun.status !== "completed") {
throw new Error(
`Run finished with status ${completedRun.status}: ${JSON.stringify(completedRun.last_error)}`
);
}
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
```
### `retrieve()`
Retrieve a run. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/getRun)
```ts example.ts
const run = await io.openai.beta.threads.runs.retrieve("get-run", "thread_abc123", "run_abc123");
```
### `cancel()`
Cancels a run that is `in_progress`. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/cancelRun)
```ts example.ts
const run = await io.openai.beta.threads.runs.cancel("cancel-run", "thread_abc123", "run_abc123");
```
### `submitToolOutputs()`
When a run has the `status: "requires_action"` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs)
```ts example.ts
const run = await io.openai.beta.threads.runs.submitToolOutputs(
"submit-tool-outputs",
"thread_abc123",
"run_abc123",
{
tool_outputs: [
{
tool_call_id: "tool_run_abc123",
output: "This is the output of the tool call.",
},
],
}
);
```
### `list()`
Returns all runs belonging to a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/listRuns)
```ts example.ts
const runs = await io.openai.beta.threads.runs.list("list-runs", "thread_abc123");
```
+38
View File
@@ -0,0 +1,38 @@
---
title: Chat Completion Tasks
sidebarTitle: Chat Completions
---
Given a list of messages comprising a conversation, the model will return a response. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat)
### `completions.create()`
Creates a model response for the given chat conversation. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/create)
```ts example.ts
await io.openai.chat.completions.create("chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
### `completions.backgroundCreate()`
Creates a model response for the given chat conversation, but runs the request in the background using [io.backgroundFetch()](/sdk/io/backgroundfetch)
```ts example.ts
await io.openai.chat.completions.create("chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
@@ -0,0 +1,19 @@
---
title: Completion Tasks
sidebarTitle: Completions (Legacy)
---
Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position. We recommend most users use the Chat Completions API. [Learn more](https://platform.openai.com/docs/deprecations/2023-07-06-gpt-and-embeddings)
### `create()`
<Warning>This is a legacy API</Warning>
Creates a completion for the provided prompt and parameters. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/completions/create)
```ts example.ts
const completion = await io.openai.completions.create("completion", {
model: "text-davinci-003",
prompt: "Create a good programming joke about Tasks",
});
```
+62
View File
@@ -0,0 +1,62 @@
---
title: File Tasks
sidebarTitle: Files
---
Files are used to upload documents that can be used with features like [Assistants](https://platform.openai.com/docs/api-reference/assistants) and [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning). [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files)
### `list()`
Returns a list of files that belong to the user's organization. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/list)
```ts example.ts
await io.openai.files.list("list-files");
await io.openai.files.list("list-files", { purpose: "assistants" }); // gets only assistant files
```
### `create()`
Upload a file that can be used across various endpoints/features. The size of all the files uploaded by one organization can be up to 100 GB.
The size of individual files for can be a maximum of `512MB`. See the [Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) to learn more about the types of files supported. The Fine-tuning API only supports `.jsonl` files.
[Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/create)
```ts example.ts
const file = await io.openai.files.create("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
```
### `createAndWaitForProcessing()`
Upload a file and will return when the file is processed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const file = await io.openai.files.createAndWaitForProcessing("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
```
### `waitForProcessing()`
Will return when the file is processed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const file = await io.openai.files.create("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
const processedFile = await io.openai.files.waitForProcessing("wait", file.id);
```
### `retrieve()`
Returns information about a specific file. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/retrieve)
```ts example.ts
await io.openai.files.retrieve("retrieve-file", "file-id");
```
@@ -0,0 +1,59 @@
---
title: Fine Tuning Tasks
sidebarTitle: Fine Tunes
---
Manage fine-tuning jobs to tailor a model to your specific training data. See the related guide [Fine Tuning models](https://platform.openai.com/docs/guides/fine-tuning) and view the [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning).
### `jobs.create()`
Creates a job that fine-tunes a specified model from a given dataset.
Response includes details of the enqueued job including job status and the name of the fine-tuned models once complete.
You must first upload a dataset to the API before creating a fine-tuning job. See our [OpenAI File Tasks](/integrations/apis/openai/files#createandwaitforprocessing) for more information.
```ts example.ts
const file = await io.openai.files.create("upload-file", {
purpose: "fine-tune",
file: fs.createReadStream("./mydata.jsonl"),
});
const fineTuning = await io.openai.fineTuning.jobs.create("fine-tuning", {
training_file: file.id,
model: "gpt-3.5-turbo",
suffix: "my-model",
});
```
### `jobs.list()`
List your organization's fine-tuning jobs. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list)
```ts example.ts
const fts = await io.openai.fineTuning.jobs.list("list");
```
### `jobs.retrieve()`
Get info about a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve)
```ts example.ts
const fineTuning = await io.openai.fineTuning.jobs.retrieve("fine-tuning", "ft_1234");
```
### `jobs.cancel()`
Immediately cancel a fine-tune job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel)
```ts example.ts
const fineTuning = await io.openai.fineTuning.jobs.cancel("fine-tuning", "ft_1234");
```
### `jobs.listEvents()`
Get status updates for a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list-events)
```ts example.ts
const events = await io.openai.fineTuning.jobs.listEvents("fine-tuning", { id: "ft_1234" });
```
+55
View File
@@ -0,0 +1,55 @@
---
title: Image Tasks
sidebarTitle: Images
---
Given a prompt and/or an input image, the model will generate a new image. See the [Image generation guide](https://platform.openai.com/docs/guides/images) and the [Official OpenAI docs](https://platform.openai.com/docs/api-reference/images).
### `create()`
Creates an image given a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/create)
```ts example.ts
await io.openai.images.create("dalle-3", {
model: "dall-e-3",
prompt:
"I would like to generate an image of an american giraffe riding a bycicle in a suburban neighborhood, into the sunset.",
});
```
### `backgroundCreate()`
Creates a an image given a prompt, but runs the request in the background using [io.backgroundFetch()](/sdk/io/backgroundfetch)
```ts example.ts
await io.openai.images.backgroundCreate("dalle-3", {
model: "dall-e-3",
prompt:
"I would like to generate an image of an american giraffe riding a bycicle in a suburban neighborhood, into the sunset.",
});
```
### `edit()`
Creates an edited or extended image given an original image and a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createEdit)
```ts example.ts
await io.openai.images.edit("dalle-2", {
model: "dall-e-2",
image: fs.createReadStream("./giraffe.jpg"),
prompt: "A painting of a giraffe in a suburban neighborhood",
response_format: "url",
});
```
### `createVariation()`
Creates a variation of a given image. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createVariation)
```ts example.ts
await io.openai.images.createVariation("dalle-3", {
model: "dall-e-2",
image: fs.createReadStream("./giraffe.jpg"),
response_format: "url",
});
```
+14
View File
@@ -0,0 +1,14 @@
---
title: Model Tasks
sidebarTitle: Models
---
List and describe the various models available in the API. You can refer to the Models documentation to understand what models are available and the differences between them. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models)
### `list`
Lists the available models. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/list)
```ts example.ts
const models = await io.openai.models.list("list-models");
```
+1 -2
View File
@@ -8,11 +8,10 @@ sidebarTitle: Overview & authentication
Plain is the customer support tool for technical teams and products.
It aims to bring engineering and customer service teams together by creating a modern opinionated platform that's fantastic to build with.
<Card
title="Jobs Showcase - Plain"
icon="rocket"
href="https://trigger.dev/showcase?tags=&integrations=plain"
href="https://trigger.dev/showcase?tags=&apis=plain"
>
Check out pre-built Plain jobs in our showcase.
</Card>
+77
View File
@@ -0,0 +1,77 @@
---
title: Resend tasks
sidebarTitle: Tasks
---
Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want.
---
## All tasks
### `sendEmail`
Send an email to a recipient with a text payload. [Official Resend Docs](https://resend.com/docs/api-reference/emails/send-email)
```ts example.ts
// Using the 'io.resend.sendEmail' method to send an email using Resend.
await io.resend.sendEmail("send-email", {
to: <recipient-email-address>, // Recipient's email address.
subject: <email-subject>, // Email subject.
text: <email-body>, // Plain text content of the email.
from: "Your-Name <your-email-address>", // Sender's email address and name.
});
}
```
## Tasks
| Function Name | Description |
| --------------- | -------------------------------- |
| `emails.send` | Send an email |
| `emails.create` | Create an email |
| `emails.get` | Get an email |
| `batch.send` | Send a batch of emails at once |
| `batch.create` | Create a batch of emails at once |
## Example
In this example we use [Zod](/documentation/guides/zod), a TypeScript-first schema declaration and validation library.
```ts
import { Resend } from "@trigger.dev/resend";
import { Job, eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
...
const resend = new Resend({
id: "resend",
apiKey: process.env.RESEND_API_KEY!,
});
client.defineJob({
id: "send-resend-email",
name: "Send Resend Email",
version: "0.1.0",
trigger: eventTrigger({
name: "send.email",
schema: z.object({
to: z.union([z.string(), z.array(z.string())]),
subject: z.string(),
text: z.string(),
}),
}),
integrations: {
resend,
},
run: async (payload, io, ctx) => {
await io.resend.emails.send("send-email", {
to: payload.to,
subject: payload.subject,
text: payload.text,
from: "Trigger.dev <hello@email.trigger.dev>",
});
},
});
```
+22 -53
View File
@@ -1,10 +1,23 @@
---
title: Resend
title: Resend overview & authentication
sidebarTitle: Overview & authentication
---
<Snippet file="integration-getting-started.mdx" />
## Overview
## Installation
Resend is the email API for developers. With our Resend integration you can send email campaigns, transactional emails, and automated emails (drip campaigns) from your app.
<Card
title="Jobs Showcase - Resend"
icon="rocket"
href="https://trigger.dev/showcase?tags=&apis=resend"
>
Check out pre-built Resend jobs in our showcase.
</Card>
## Installing the Resend packages
To get started with our Resend integration, you need to install the `@trigger.dev/resend` packages. You can do this using `npm`, `pnpm`, or `yarn`:
<CodeGroup>
@@ -24,7 +37,7 @@ yarn add @trigger.dev/resend@latest
## Authentication
Resend supports API Keys
Resend supports API Keys. Official documentation [here](https://resend.com/docs/api-reference/api-keys/create-api-key).
```ts
import { Resend } from "@trigger.dev/resend";
@@ -35,54 +48,10 @@ const resend = new Resend({
});
```
## Example
In this example we use [Zod](/documentation/guides/zod), a TypeScript-first schema declaration and validation library.
```ts
import { Resend } from "@trigger.dev/resend";
import { Job, eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
...
const resend = new Resend({
id: "resend",
apiKey: process.env.RESEND_API_KEY!,
});
client.defineJob({
id: "send-resend-email",
name: "Send Resend Email",
version: "0.1.0",
trigger: eventTrigger({
name: "send.email",
schema: z.object({
to: z.union([z.string(), z.array(z.string())]),
subject: z.string(),
text: z.string(),
}),
}),
integrations: {
resend,
},
run: async (payload, io, ctx) => {
await io.resend.emails.send("send-email", {
to: payload.to,
subject: payload.subject,
text: payload.text,
from: "Trigger.dev <hello@email.trigger.dev>",
});
},
});
```
## Tasks
| Function Name | Description |
| --------------- | -------------------------------- |
| `emails.send` | Send an email |
| `emails.create` | Create an email |
| `emails.get` | Get an email |
| `batch.send` | Send a batch of emails at once |
| `batch.create` | Create a batch of emails at once |
Once you have set up a Resend client, you can use it to create tasks.
<Card title="Tasks" icon="sparkles" href="/integrations/apis/resend-tasks">
Send emails with Resend.
</Card>
+89
View File
@@ -0,0 +1,89 @@
---
title: SendGrid tasks
sidebarTitle: Tasks
---
Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want.
---
## All tasks
### `sendEmail`
Send an email with a body. [Official SendGrid Docs](https://docs.sendgrid.com/for-developers/sending-email)
```ts example.ts
// Using the 'io.sendgrid.sendEmail' method to send an email using SendGrid.
await io.sendgrid.sendEmail({
to: <recipient-email-address>, // Recipient's email address.
from: "Your-Name <your-email-address>", // Sender's email address and name.
subject: <email-subject>, // Email subject.
text: <email-body>, // Plain text content of the email.
});
}
```
## Example usage
In this example, we will send weekly summary emails to users who have `summariesEnabled = true`, at 4pm every Friday, and then posts the total numbers to Slack.
```ts example.ts
import { TriggerClient, cronTrigger } from "@trigger.dev/sdk";
import { SendGrid } from "@trigger.dev/sendgrid";
import { Slack } from "@trigger.dev/slack";
import { weeklySummaryDb } from "./mocks/db";
import { weeklySummaryEmail } from "./mocks/emails";
// Creating instances of SendGrid and Slack clients.
const sendgrid = new SendGrid({
id: "sendgrid",
apiKey: process.env.SENDGRID_API_KEY!,
});
const slack = new Slack({ id: "slack" });
// Defining a job that sends a weekly summary email to users and posts total numbers to Slack.
client.defineJob({
id: "weekly-user-activity-summary", // Unique identifier for the job.
name: "Weekly user activity summary", // A specific name for the job.
version: "1.0.0", // Version number for the job.
integrations: { sendgrid, slack }, // Integrating SendGrid and Slack clients into this job.
trigger: cronTrigger({
// Setting a cron schedule to run the job every Friday at 4 pm.
cron: "0 16 * * 5",
}),
run: async (payload, io, ctx) => {
// Inside the 'run' function, several operations are performed to send weekly summaries and post to Slack.
// Retrieving a list of users from the weeklySummaryDb.
const users = await weeklySummaryDb.getUsers();
let sentCount = 0;
let notSentCount = 0;
// Iterating through the list of users.
for (const user of users) {
if (user.summariesEnabled) {
// Sending a weekly summary email to users with summaries enabled.
await io.sendgrid.sendEmail(`Weekly summary for ${user.id}`, {
to: user.email,
from: "hello@acme.inc", // The sender's email.
subject: "Your weekly summary",
html: weeklySummaryEmail(user), // HTML content for the email.
});
sentCount++;
} else {
notSentCount++;
}
}
// Posting a message to Slack with a summary of the sent and unsent emails.
await io.slack.postMessage("Notify team", {
text: `Weekly summary sent to ${sentCount} users and not sent to ${notSentCount} users`,
channel: "YOUR_CHANNEL_ID", // Specify the Slack channel ID for posting the message.
});
},
});
```
+24 -49
View File
@@ -1,10 +1,23 @@
---
title: SendGrid
title: SendGrid overview & authentication
sidebarTitle: Overview & authentication
---
<Snippet file="integration-getting-started.mdx" />
## Overview
## Installation
SendGrid is a cloud-based SMTP provider that allows you to send email without having to maintain email servers. With our SendGrid integration you can send email campaigns, transactional emails, and automated emails (drip campaigns) from your app.
<Card
title="Jobs Showcase - SendGrid"
icon="rocket"
href="https://trigger.dev/showcase?tags=&apis=sendgrid"
>
Check out pre-built SendGrid jobs in our showcase.
</Card>
## Installing the SendGrid packages
To get started with our SendGrid integration, you need to install the `@trigger.dev/sendgrid` packages. You can do this using `npm`, `pnpm`, or `yarn`:
<CodeGroup>
@@ -24,7 +37,7 @@ yarn add @trigger.dev/sendgrid@latest
## Authentication
SendGrid integration supports API Keys. To authenticate, you'll need to create an instance of the SendGrid class and provide your API key.
SendGrid supports API Keys. Official documentation [here](https://docs.sendgrid.com/ui/account-and-settings/api-keys).
```ts
import { SendGrid } from "@trigger.dev/sendgrid";
@@ -35,50 +48,12 @@ const sendgrid = new SendGrid({
});
```
## Example
In this example we use [Zod](/documentation/guides/zod), a TypeScript-first schema declaration and validation library.
```ts
import { SendGrid } from "@trigger.dev/sendgrid";
import { Job, eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
// Create an instance of SendGrid
const sendgrid = new SendGrid({
id: "sendgrid",
apiKey: process.env.SENDGRID_API_KEY!,
});
// Define a Trigger.dev job
client.defineJob({
id: "send-sendgrid-email",
name: "Send SendGrid Email",
version: "0.1.0",
trigger: eventTrigger({
name: "send.email",
schema: z.object({
to: z.string(),
subject: z.string(),
text: z.string(),
}),
}),
integrations: {
sendgrid,
},
run: async (payload, io, ctx) => {
await io.sendgrid.sendEmail({
to: payload.to,
from: "Trigger.dev <hello@email.trigger.dev>",
subject: payload.subject,
text: payload.text,
});
},
});
```
## Tasks
| Function Name | Description |
| ------------- | ------------- |
| `sendEmail` | Send an email |
Once you have set up a SendGrid client, you can use it to create tasks.
{" "}
<Card title="Tasks" icon="sparkles" href="/integrations/apis/sendgrid-tasks">
Send emails with SendGrid.
</Card>

Some files were not shown because too many files have changed in this diff Show More