Compare commits

...

19 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
142 changed files with 5045 additions and 999 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);
+9
View File
@@ -49,6 +49,15 @@ const EnvironmentSchema = z.object({
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(),
});
+7 -2
View File
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
type EventSourceOptions = {
init?: EventSourceInit;
event?: string;
disabled?: boolean;
};
/**
@@ -13,11 +14,15 @@ type EventSourceOptions = {
*/
export function useEventSource(
url: string | URL,
{ event = "message", init }: EventSourceOptions = {}
{ 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);
@@ -32,7 +37,7 @@ export function useEventSource(
eventSource.removeEventListener(event ?? "message", handler);
eventSource.close();
};
}, [url, event, init]);
}, [url, event, init, disabled]);
return data;
}
+7 -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,7 @@ 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,
@@ -32,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(
@@ -105,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 -4
View File
@@ -14,7 +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 { 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>;
@@ -270,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,
@@ -282,6 +282,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
payload,
spec,
job,
durationInMs,
});
return job;
@@ -304,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,
@@ -327,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) {
@@ -337,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) {
@@ -471,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(
@@ -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,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);
@@ -82,7 +82,7 @@ export default function Integrations() {
<PageContainer>
<PageHeader>
<PageTitleRow>
<PageTitle title="Integrations" />
<PageTitle title="Integrations & APIs" />
<PageButtons>
<LinkButton
to={docsPath("/integrations/introduction")}
@@ -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} />;
}
@@ -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 { 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} />;
}
@@ -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,
},
@@ -2,21 +2,16 @@ import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import {
API_VERSIONS,
RunTaskBodyOutput,
RunTaskBodyOutputSchema,
RunTaskResponseWithCachedTasksBody,
ServerTask,
} from "@trigger.dev/core";
import { TaskStatus } from "@trigger.dev/database";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { env } from "~/env.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 { generateSecret } from "~/services/sources/utils.server";
import { ulid } from "~/services/ulid.server";
import { taskOperationWorker, workerQueue } from "~/services/worker.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 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;
});
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" />
);
}
+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 };
}
@@ -1,7 +1,6 @@
import { PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { taskOperationWorker } from "../worker.server";
import { EphemeralDispatchableSchema } from "~/models/eventDispatcher.server";
import { fetch } from "@whatwg-node/fetch";
import { ExpireDispatcherService } from "./expireDispatcher.server";
export class InvokeEphemeralDispatcherService {
@@ -61,23 +60,14 @@ export class InvokeEphemeralDispatcherService {
accountId: eventRecord.externalAccount ? eventRecord.externalAccount.identifier : undefined,
};
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort();
}, 5000);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(body),
signal: abortController.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(
`Failed to invoke ephemeral dispatcher: ${response.statusText} [${response.status}]`
@@ -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) {
@@ -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,
});
+17 -3
View File
@@ -1,18 +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";
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
sensitiveDataReplacer,
() => {
const fields = currentFieldsStore.getStore();
return fields ? { ...fields } : {};
}
);
export const workerLogger = new Logger(
"worker",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString"],
sensitiveDataReplacer
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,
@@ -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";
});
}
}
+10 -5
View File
@@ -442,11 +442,16 @@ 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
);
},
},
},
+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}`;
}
+18 -12
View File
@@ -22,6 +22,16 @@ export function sse({ request, pingInterval = 1000, updateInterval = 348, run }:
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 = () => {
clearInterval(pinger);
clearInterval(updater);
clearTimeout(timeout);
};
return eventStream(request.signal, (send, close) => {
const safeSend = (args: { event?: string; data: string }) => {
try {
@@ -49,30 +59,26 @@ export function sse({ request, pingInterval = 1000, updateInterval = 348, run }:
}
};
const pinger = setInterval(() => {
pinger = setInterval(() => {
if (request.signal.aborted) {
return close();
return abort();
}
safeSend({ event: "ping", data: new Date().toISOString() });
}, pingInterval);
const updater = setInterval(() => {
updater = setInterval(() => {
if (request.signal.aborted) {
return close();
return abort();
}
run(safeSend, close);
run(safeSend, abort);
}, updateInterval);
const timeout = setTimeout(() => {
close();
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 () => {
clearInterval(updater);
clearInterval(pinger);
clearTimeout(timeout);
};
return abort;
});
}
+3 -1
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:*",
@@ -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",
+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.
@@ -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
+1 -1
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>
+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>
+6 -8
View File
@@ -7,12 +7,10 @@ sidebarTitle: Overview & authentication
Our Slack integration allows you to connect to the Slack API and post messages to Slack.
For examples of some of the things you can do with Slack, check out our Jobs Showcase:
<Card
title="Jobs Showcase - Slack"
icon="rocket"
href="https://trigger.dev/showcase?tags=&integrations=slack"
href="https://trigger.dev/showcase?tags=&apis=slack"
>
Check out pre-built Slack jobs in our showcase.
</Card>
@@ -55,8 +53,8 @@ const slack = new Slack({
Once you have set up a Slack client, you can use it to create tasks.
<CardGroup>
<Card title="Tasks" icon="sparkles" href="/integrations/apis/slack-tasks">
Perform tasks such as posting messages to a channel.
</Card>
</CardGroup>
{" "}
<Card title="Tasks" icon="sparkles" href="/integrations/apis/slack-tasks">
Perform tasks such as posting messages to a channel.
</Card>
+3 -2
View File
@@ -22,8 +22,9 @@ description: "Integrations make it easy to authenticate and use APIs."
<Card title="How to create your own Integrations" icon="paintbrush" href="/integrations/create">
You can create your own Integrations (and optionally share them with others)
</Card>
<Card title="Example repo & Jobs" icon="code" href="/examples/introduction">
Check out example Jobs using our integrations.{" "}
<Card title="API catalog with code examples" icon="code" href="https://trigger.dev/apis">
Use any API with an existing Node SDK or even just using fetch. Find the right API for your
project here.
</Card>
</CardGroup>
+19 -33
View File
@@ -1,9 +1,7 @@
{
"$schema": "https://mintlify.com/schema.json",
"name": "Trigger.dev",
"openapi": [
"/openapi.yml"
],
"openapi": ["/openapi.yml"],
"logo": {
"dark": "/logo/dark.png",
"light": "/logo/light.png",
@@ -255,10 +253,7 @@
"pages": [
{
"group": "Airtable",
"pages": [
"integrations/apis/airtable",
"integrations/apis/airtable-tasks"
]
"pages": ["integrations/apis/airtable", "integrations/apis/airtable-tasks"]
},
{
"group": "GitHub",
@@ -284,20 +279,20 @@
},
{
"group": "Plain",
"pages": [
"integrations/apis/plain",
"integrations/apis/plain-tasks"
]
"pages": ["integrations/apis/plain", "integrations/apis/plain-tasks"]
},
"integrations/apis/replicate",
"integrations/apis/resend",
"integrations/apis/sendgrid",
{
"group": "SendGrid",
"pages": ["integrations/apis/sendgrid", "integrations/apis/sendgrid-tasks"]
},
{
"group": "Resend",
"pages": ["integrations/apis/resend", "integrations/apis/resend-tasks"]
},
{
"group": "Slack",
"pages": [
"integrations/apis/slack",
"integrations/apis/slack-tasks"
]
"pages": ["integrations/apis/slack", "integrations/apis/slack-tasks"]
},
"integrations/apis/stripe",
{
@@ -334,7 +329,8 @@
"sdk/triggerclient/instancemethods/define-http-endpoint",
"sdk/triggerclient/instancemethods/define-dynamic-trigger",
"sdk/triggerclient/instancemethods/define-dynamic-schedule",
"sdk/triggerclient/instancemethods/define-auth-resolver"
"sdk/triggerclient/instancemethods/define-auth-resolver",
"sdk/triggerclient/instancemethods/on"
]
}
]
@@ -377,10 +373,7 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
}
]
},
@@ -391,10 +384,7 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
}
]
},
@@ -406,9 +396,7 @@
},
{
"group": "HTTP Reference",
"pages": [
"sdk/api-reference/events/create-an-event"
]
"pages": ["sdk/api-reference/events/create-an-event"]
},
{
"group": "React SDK",
@@ -422,9 +410,7 @@
},
{
"group": "Overview",
"pages": [
"examples/introduction"
]
"pages": ["examples/introduction"]
}
],
"footerSocials": {
@@ -437,4 +423,4 @@
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
}
}
}
}
-2
View File
@@ -4,8 +4,6 @@ sidebarTitle: "waitForEvent()"
description: "`io.waitForEvent()` waits for the next event to be emitted, and returns the event data"
---
<Warning>This feature is in beta and has not yet been deployed to the Trigger.dev cloud</Warning>
## Parameters
<Snippet file="stable-key-param.mdx" />
+23
View File
@@ -26,6 +26,29 @@ client.defineJob({
});
```
```ts notifications
client.defineJob({
id: "github-integration-on-issue",
name: "GitHub Integration - On Issue",
version: "0.1.0",
trigger: github.triggers.repo({
event: events.onIssue,
owner: "triggerdotdev",
repo: "empty",
}),
onSuccess: async (notification) => {
console.log("Job succeeded", notification);
},
onFailure: async (notification) => {
console.log("Job failed", notification);
},
run: async (payload, io, ctx) => {
await io.logger.info("This is a simple log info message");
return { payload, ctx };
},
});
```
</RequestExample>
# Constructor
+48
View File
@@ -40,6 +40,9 @@ This document describes the payload of a Run's completion webhook.
<ResponseField name="completedAt" type="Date" required>
When the Run started
</ResponseField>
<ResponseField name="isTest" type="boolean" required>
Whether the Run was a test Run
</ResponseField>
<ResponseField name="executionDurationInMs" type="number" required>
The duration of the Run in milliseconds
</ResponseField>
@@ -47,6 +50,24 @@ This document describes the payload of a Run's completion webhook.
The number of individual function executions performed to complete the Run
</ResponseField>
<ResponseField name="invocation" type="object" required>
Metadata about the Invocation / Run
<Expandable title="properties">
<ResponseField name="id" type="string" required>
The Run ID
</ResponseField>
<ResponseField name="payload" type="any" required>
The payload that was passed to the Run
</ResponseField>
<ResponseField name="timestamp" type="Date" required>
The timestamp of the event/Run
</ResponseField>
<ResponseField name="context" type="any">
The context that was passed to the Run
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="job" type="object" required>
Metadata about the Job
<Expandable title="properties">
@@ -59,6 +80,33 @@ This document describes the payload of a Run's completion webhook.
</Expandable>
</ResponseField>
<ResponseField name="task" type="object">
Metadata about the Task that failed, only exists if the Run failed
<Expandable title="properties">
<ResponseField name="id" type="string" required>
The Task ID
</ResponseField>
<ResponseField name="cacheKey" type="string">
The Task's cache key
</ResponseField>
<ResponseField name="error" type="Error" required>
And Error-like object that caused the Task to fail
</ResponseField>
<ResponseField name="status" type="string" required>
The Task's status
</ResponseField>
<ResponseField name="name" type="string">
The Task's name
</ResponseField>
<ResponseField name="startedAt" type="Date">
When the Task started
</ResponseField>
<ResponseField name="params" type="any">
The Task's params
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="environment" type="object" required>
Metadata about the Environment
<Expandable title="properties">
@@ -24,6 +24,29 @@ client.defineJob({
});
```
```ts notifications
client.defineJob({
id: "github-integration-on-issue",
name: "GitHub Integration - On Issue",
version: "0.1.0",
trigger: github.triggers.repo({
event: events.onIssue,
owner: "triggerdotdev",
repo: "empty",
}),
onSuccess: async (notification) => {
console.log("Job succeeded", notification);
},
onFailure: async (notification) => {
console.log("Job failed", notification);
},
run: async (payload, io, ctx) => {
await io.logger.info("This is a simple log info message");
return { payload, ctx };
},
});
```
</RequestExample>
## Parameters
@@ -0,0 +1,46 @@
---
title: "TriggerClient: on() instance method"
sidebarTitle: "on()"
description: "Use the `on()` method to listen for run notifications across all Jobs."
---
You can subscribe to run notifications across all your Jobs using the `on()` instance method, which currently supports two events:
- `runSucceeded`: Triggered when a run succeeds.
- `runFailed`: Triggered when a run fails.
Both events receive a [Run Notification](/sdk/run-notification) object as their only parameter.
You can use the notification to determine which job the run belongs to, access the run's payload, output, errors, and more.
If you want to subscribe to just a single Job, see the [defineJob](/sdk/triggerclient/instancemethods/define-job) docs.
## Parameters
<ParamField body="event" type="runSucceeded | runFailed" required>
The notification event to listen for.
</ParamField>
<ParamField body="callback" type="function" required>
The callback function to run when the event is triggered. Receives a [Run
Notification](/sdk/run-notification) object as it's only parameter.
</ParamField>
<RequestExample>
```ts example
export const client = new TriggerClient({
id: "my-project",
apiKey: process.env.TRIGGER_API_KEY,
});
client.on("runSucceeeded", async (notification) => {
console.log(`Run on job ${notification.job.id} succeeded`);
});
client.on("runFailed", async (notification) => {
console.log(`Run on job ${notification.job.id} failed`);
});
```
</RequestExample>
+4
View File
@@ -83,3 +83,7 @@ The `defineDynamicSchedule()` method defines a new Dynamic Schedule.
#### [defineAuthResolver()](/sdk/triggerclient/instancemethods/define-auth-resolver)
The `defineAuthResolver()` method defines a new Auth Resolver.
#### [on()](/sdk/triggerclient/instancemethods/on)
Use the `on()` method to listen for run notifications across all Jobs.
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/airtable
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "2.2.6",
"version": "2.2.7",
"description": "Trigger.dev integration for airtable",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.7",
"@trigger.dev/sdk": "workspace:^2.2.7",
"airtable": "^0.12.1",
"zod": "3.22.3"
},
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/github
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "2.2.6",
"version": "2.2.7",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -29,8 +29,8 @@
"@octokit/request": "^6.2.5",
"@octokit/request-error": "^4.0.1",
"@octokit/webhooks": "^10.4.0",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.7",
"@trigger.dev/sdk": "workspace:^2.2.7",
"octokit": "^2.0.14",
"zod": "3.22.3"
},
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/linear
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/linear",
"version": "2.2.6",
"version": "2.2.7",
"description": "Trigger.dev integration for @linear/sdk",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -27,8 +27,8 @@
},
"dependencies": {
"@linear/sdk": "^8.0.0",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.7",
"@trigger.dev/sdk": "workspace:^2.2.7",
"zod": "3.22.3"
},
"engines": {
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/slack
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "2.2.6",
"version": "2.2.7",
"description": "The official OpenAI integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -31,8 +31,8 @@
},
"dependencies": {
"openai": "^4.16.1",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.6"
"@trigger.dev/sdk": "workspace:^2.2.7",
"@trigger.dev/integration-kit": "workspace:^2.2.7"
},
"engines": {
"node": ">=18.0.0"
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/plain
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "2.2.6",
"version": "2.2.7",
"description": "The official Plain.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.7",
"@trigger.dev/sdk": "workspace:^2.2.7",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/replicate
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/replicate",
"version": "2.2.6",
"version": "2.2.7",
"description": "Trigger.dev integration for replicate",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.7",
"@trigger.dev/sdk": "workspace:^2.2.7",
"replicate": "^0.18.1",
"zod": "3.22.3"
},
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/resend
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "2.2.6",
"version": "2.2.7",
"description": "The official Resend.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.7",
"@trigger.dev/sdk": "workspace:^2.2.7",
"resend": "^2.0.0"
},
"engines": {
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/sendgrid
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "2.2.6",
"version": "2.2.7",
"description": "Trigger.dev integration for @sendgrid/mail",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -27,8 +27,8 @@
},
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.6"
"@trigger.dev/sdk": "workspace:^2.2.7",
"@trigger.dev/integration-kit": "workspace:^2.2.7"
},
"engines": {
"node": ">=16.8.0"
+7
View File
@@ -1,5 +1,12 @@
# @trigger.dev/slack
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
## 2.2.6
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "2.2.6",
"version": "2.2.7",
"description": "The official Slack integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
},
"dependencies": {
"@slack/web-api": "^6.8.1",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.7",
"zod": "3.22.3"
},
"engines": {
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/stripe
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "2.2.6",
"version": "2.2.7",
"description": "Trigger.dev integration for stripe",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.7",
"@trigger.dev/sdk": "workspace:^2.2.7",
"stripe": "^12.14.0",
"zod": "3.22.3"
},
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/supabase
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/supabase",
"version": "2.2.6",
"version": "2.2.7",
"description": "Trigger.dev integration for @supabase/supabase-js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -27,8 +27,8 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.26.0",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.7",
"@trigger.dev/sdk": "workspace:^2.2.7",
"supabase-management-js": "^0.1.4",
"zod": "3.22.3"
},
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/typeform
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
- @trigger.dev/integration-kit@2.2.7
## 2.2.6
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/typeform",
"version": "2.2.6",
"version": "2.2.7",
"description": "The official Typeform integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.7",
"@trigger.dev/sdk": "workspace:^2.2.7",
"@typeform/api-client": "^1.8.0",
"zod": "3.22.3"
},
+7
View File
@@ -1,5 +1,12 @@
# @trigger.dev/astro
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/sdk@2.2.7
## 2.2.6
### Patch Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@trigger.dev/astro",
"description": "An Astro-native integration for Trigger.dev background jobs platform",
"version": "2.2.6",
"version": "2.2.7",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
@@ -20,7 +20,7 @@
"build:tsup": "tsup"
},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^2.2.6"
"@trigger.dev/sdk": "workspace:^2.2.7"
},
"devDependencies": {
"astro": "^3.0.12",
+7
View File
@@ -1,5 +1,12 @@
# create-trigger
## 2.2.7
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/core@2.2.7
## 2.2.6
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/cli",
"version": "2.2.6",
"version": "2.2.7",
"description": "The Trigger.dev CLI",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

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