{children}
diff --git a/apps/webapp/app/components/triggers/Trigger.tsx b/apps/webapp/app/components/triggers/Trigger.tsx
index 2335eae3588..b31d2f27561 100644
--- a/apps/webapp/app/components/triggers/Trigger.tsx
+++ b/apps/webapp/app/components/triggers/Trigger.tsx
@@ -36,6 +36,7 @@ function Webhook({ webhook }: { webhook: WebhookEventTrigger }) {
{webhook.source &&
+ !webhook.manualRegistration &&
Object.entries(webhook.source).map(([key, value]) => (
diff --git a/apps/webapp/app/components/triggers/TriggerIcons.tsx b/apps/webapp/app/components/triggers/TriggerIcons.tsx
index 58fd0676527..4dc4e0c4879 100644
--- a/apps/webapp/app/components/triggers/TriggerIcons.tsx
+++ b/apps/webapp/app/components/triggers/TriggerIcons.tsx
@@ -47,6 +47,14 @@ export function TriggerTypeIcon({
return (

);
+ case "WEBHOOK":
+ return (
+

+ );
default:
return null;
}
diff --git a/apps/webapp/app/entry.server.tsx b/apps/webapp/app/entry.server.tsx
index 5a5ec14bac9..f06ef773c91 100644
--- a/apps/webapp/app/entry.server.tsx
+++ b/apps/webapp/app/entry.server.tsx
@@ -3,6 +3,7 @@ import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
import * as Sentry from "~/services/sentry.server";
import * as MessageBroker from "~/services/messageBroker.server";
+import * as Triggers from "~/triggers/init.server";
export default function handleRequest(
request: Request,
@@ -26,3 +27,4 @@ export default function handleRequest(
Sentry.init();
MessageBroker.init();
+Triggers.init();
diff --git a/apps/webapp/app/models/externalSource.server.ts b/apps/webapp/app/models/externalSource.server.ts
index 6099949629c..c9a976c1798 100644
--- a/apps/webapp/app/models/externalSource.server.ts
+++ b/apps/webapp/app/models/externalSource.server.ts
@@ -1,5 +1,8 @@
import { prisma } from "~/db.server";
-export type { ExternalSource } from ".prisma/client";
+import type { ExternalSource } from ".prisma/client";
+import { env } from "process";
+
+export type { ExternalSource };
export type ExternalSourceWithConnection = Awaited<
ReturnType
@@ -31,3 +34,7 @@ export async function connectExternalSource({
},
});
}
+
+export function buildExternalSourceUrl(id: string, serviceIdentifier: string) {
+ return `${env.APP_ORIGIN}/api/v1/internal/webhooks/${serviceIdentifier}/${id}`;
+}
diff --git a/apps/webapp/app/models/workflow.server.ts b/apps/webapp/app/models/workflow.server.ts
index be3393c3f9d..91ef3b79f49 100644
--- a/apps/webapp/app/models/workflow.server.ts
+++ b/apps/webapp/app/models/workflow.server.ts
@@ -33,6 +33,8 @@ export function getWorkflowFromSlugs({
connection: true,
key: true,
service: true,
+ manualRegistration: true,
+ secret: true,
},
},
externalServices: {
diff --git a/apps/webapp/app/models/workflowRunPresenter.server.ts b/apps/webapp/app/models/workflowRunPresenter.server.ts
index 8fd59581393..6d13a489a8b 100644
--- a/apps/webapp/app/models/workflowRunPresenter.server.ts
+++ b/apps/webapp/app/models/workflowRunPresenter.server.ts
@@ -125,7 +125,9 @@ async function parseStep(
const fetchRequest = FetchRequestSchema.parse(original.input);
const lastFetchResponse = original.fetchRequest.responses[0];
const lastResponse = lastFetchResponse
- ? FetchResponseSchema.parse(lastFetchResponse.output)
+ ? FetchResponseSchema.safeParse(lastFetchResponse.output).success
+ ? FetchResponseSchema.parse(lastFetchResponse.output)
+ : undefined
: undefined;
return {
diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx
index 10350dd6983..1f51c81cd6f 100644
--- a/apps/webapp/app/root.tsx
+++ b/apps/webapp/app/root.tsx
@@ -143,7 +143,7 @@ function App() {
background: "#D1FAE5",
padding: "16px 20px",
color: "#1E293B",
- minWidth: "500px",
+ maxWidth: "500px",
},
iconTheme: {
primary: "#10B981",
@@ -157,7 +157,7 @@ function App() {
background: "#FFE4E6",
padding: "16px 20px",
color: "#1E293B",
- minWidth: "500px",
+ maxWidth: "500px",
},
iconTheme: {
primary: "#F43F5E",
diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx
index c9cd12ff68f..c9a3d4a41a4 100644
--- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx
+++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug.tsx
@@ -1,6 +1,9 @@
import { Outlet } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
-import { TriggerMetadataSchema } from "@trigger.dev/common-schemas";
+import {
+ ManualWebhookSourceSchema,
+ TriggerMetadataSchema,
+} from "@trigger.dev/common-schemas";
import { typedjson } from "remix-typedjson";
import invariant from "tiny-invariant";
import { Container } from "~/components/layout/Container";
@@ -9,6 +12,7 @@ import {
WorkflowsSideMenu,
} from "~/components/navigation/SideMenu";
import { getConnectedApiConnectionsForOrganizationSlug } from "~/models/apiConnection.server";
+import { buildExternalSourceUrl } from "~/models/externalSource.server";
import { getIntegrations } from "~/models/integrations.server";
import { getRuntimeEnvironmentFromRequest } from "~/models/runtimeEnvironment.server";
import { getWorkflowFromSlugs } from "~/models/workflow.server";
@@ -78,8 +82,28 @@ export const loader = async ({ request, params }: LoaderArgs) => {
}),
};
+ const externalSourceSecret =
+ workflow.externalSource &&
+ workflow.externalSource.manualRegistration &&
+ ManualWebhookSourceSchema.safeParse(workflow.externalSource.source)
+ .success &&
+ ManualWebhookSourceSchema.parse(workflow.externalSource.source)
+ .verifyPayload.enabled
+ ? workflow.externalSource.secret
+ : undefined;
+
return typedjson({
- workflow: { ...workflow, rules },
+ workflow: {
+ ...workflow,
+ rules,
+ externalSourceUrl: workflow.externalSource
+ ? buildExternalSourceUrl(
+ workflow.externalSource.id,
+ workflow.externalSource.service
+ )
+ : undefined,
+ externalSourceSecret: externalSourceSecret,
+ },
currentEnvironmentSlug,
connectionSlots,
});
diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/index.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/index.tsx
index e86acde79a6..c1d3299a7dd 100644
--- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/index.tsx
+++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/index.tsx
@@ -1,6 +1,8 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import invariant from "tiny-invariant";
+import CodeBlock from "~/components/code/CodeBlock";
+import { CopyTextButton } from "~/components/CopyTextButton";
import { WorkflowConnections } from "~/components/integrations/WorkflowConnections";
import { Panel } from "~/components/layout/Panel";
import { PanelHeader } from "~/components/layout/PanelHeader";
@@ -11,6 +13,7 @@ import {
SecondaryLink,
TertiaryLink,
} from "~/components/primitives/Buttons";
+import { Input } from "~/components/primitives/Input";
import { Body } from "~/components/primitives/text/Body";
import { SubTitle } from "~/components/primitives/text/SubTitle";
import { Title } from "~/components/primitives/text/Title";
@@ -86,9 +89,61 @@ export default function Page() {
{workflow.status === "CREATED" && (
<>
-
- This workflow requires its APIs to be connected before it can run.
-
+ {eventRule &&
+ eventRule.trigger.type === "WEBHOOK" &&
+ eventRule.trigger.manualRegistration &&
+ workflow.externalSourceUrl ? (
+
+
+
+ Use these details to register your webhook – this usually
+ involves logging in to the developer section of the service.
+
+
+
+ {workflow.externalSourceSecret && (
+
+ )}
+
+
+
+ ) : (
+
+ This workflow requires its APIs to be connected before it can run.
+
+ )}
>
)}
{workflow.status === "DISABLED" && (
diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx
index 91f0912bac6..f17914345af 100644
--- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx
+++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx
@@ -67,7 +67,9 @@ export default function Page() {
workflowSlug={workflow.slug}
eventNames={workflow.eventNames}
initialValue={
- latestRun == null
+ workflow.type === "SCHEDULE"
+ ? JSON.stringify({ scheduledTime: new Date() }, null, 2)
+ : latestRun == null
? "{\n\n}"
: JSON.stringify(latestRun.event.payload, null, 2)
}
diff --git a/apps/webapp/app/routes/api/v1/internal/webhooks/$serviceIdentifier.$id.ts b/apps/webapp/app/routes/api/v1/internal/webhooks/$serviceIdentifier.$id.ts
index c6b0b8072cb..e318a4ee3a2 100644
--- a/apps/webapp/app/routes/api/v1/internal/webhooks/$serviceIdentifier.$id.ts
+++ b/apps/webapp/app/routes/api/v1/internal/webhooks/$serviceIdentifier.$id.ts
@@ -17,7 +17,10 @@ export async function action({ request, params }: ActionArgs) {
};
}
- if (externalSource.connection?.apiIdentifier !== serviceIdentifier) {
+ if (
+ !externalSource.manualRegistration &&
+ externalSource.connection?.apiIdentifier !== serviceIdentifier
+ ) {
return { status: 500, body: "Service identifier does not match" };
}
diff --git a/apps/webapp/app/routes/api/v1/internal/workflows/$workflowP.ts b/apps/webapp/app/routes/api/v1/internal/workflows/$workflowP.ts
index a7eab000af0..90d3d62b291 100644
--- a/apps/webapp/app/routes/api/v1/internal/workflows/$workflowP.ts
+++ b/apps/webapp/app/routes/api/v1/internal/workflows/$workflowP.ts
@@ -34,8 +34,10 @@ export async function action({ request, params }: ActionArgs) {
);
switch (result.status) {
- case "validationError":
- return json({ error: result.data }, { status: 400 });
+ case "validationError": {
+ return json({ error: result.errors }, { status: 400 });
+ }
+
case "success":
return json(result.data);
}
diff --git a/apps/webapp/app/services/events/dispatch.server.ts b/apps/webapp/app/services/events/dispatch.server.ts
index f266f8e4276..7aa3366a96a 100644
--- a/apps/webapp/app/services/events/dispatch.server.ts
+++ b/apps/webapp/app/services/events/dispatch.server.ts
@@ -5,6 +5,7 @@ import { prisma } from "~/db.server";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import type { Workflow } from "~/models/workflow.server";
import { taskQueue } from "../messageBroker.server";
+import { generateErrorMessage } from "zod-error";
export class DispatchEvent {
#prismaClient: PrismaClient;
@@ -41,8 +42,16 @@ export class DispatchEvent {
},
});
+ console.log(
+ `Found ${eventRules.length} event rules to check for event ${
+ event.id
+ }: ${eventRules.map((eventRule) => eventRule.id).join(", ")}`
+ );
+
const matcher = new EventMatcher(event);
+ console.log(`Matching event rules for event`, matcher.json);
+
const matchingEventRules = eventRules.filter((eventRule) => {
return matcher.matches(eventRule);
});
@@ -77,20 +86,28 @@ export class DispatchEvent {
}
class EventMatcher {
- #json: any;
+ json: any;
constructor(event: TriggerEvent) {
- this.#json = this.#createEventJsonFromEvent(event);
+ this.json = this.#createEventJsonFromEvent(event);
}
public matches(eventRule: EventRule) {
+ console.log(`Matching against event rule ${eventRule.id}`);
+
const filter = this.#parseFilter(eventRule);
if (!filter.success) {
+ console.error(
+ `Could not parse filter for event rule ${
+ eventRule.id
+ }, returning false: ${generateErrorMessage(filter.error.issues)}`
+ );
+
return false;
}
- return patternMatches(this.#json, filter.data);
+ return patternMatches(this.json, filter.data);
}
#parseFilter(eventRule: EventRule) {
@@ -104,6 +121,7 @@ class EventMatcher {
event: event.name,
service: event.service,
payload: event.payload,
+ context: event.context,
};
}
}
@@ -165,8 +183,40 @@ export class DispatchWorkflowRun {
status: "PENDING",
isTest: event.isTest,
},
+ include: {
+ workflow: {
+ include: {
+ externalSource: true,
+ },
+ },
+ },
});
+ if (
+ workflowRun.workflow.externalSource &&
+ workflowRun.workflow.externalSource.status === "CREATED"
+ ) {
+ await this.#prismaClient.externalSource.update({
+ where: {
+ id: workflowRun.workflow.externalSource.id,
+ },
+ data: {
+ status: "READY",
+ },
+ });
+
+ if (workflowRun.workflow.status === "CREATED") {
+ await this.#prismaClient.workflow.update({
+ where: {
+ id: workflowRun.workflow.id,
+ },
+ data: {
+ status: "READY",
+ },
+ });
+ }
+ }
+
console.log(
`Created workflow run ${workflowRun.id} for event rule ${eventRule.id}`
);
diff --git a/apps/webapp/app/services/externalSources/handleExternalSource.server.ts b/apps/webapp/app/services/externalSources/handleExternalSource.server.ts
index 39c166d776b..7a5de15838b 100644
--- a/apps/webapp/app/services/externalSources/handleExternalSource.server.ts
+++ b/apps/webapp/app/services/externalSources/handleExternalSource.server.ts
@@ -4,6 +4,9 @@ import { github } from "internal-integrations";
import type { ExternalSourceWithConnection } from "~/models/externalSource.server";
import type { NormalizedRequest } from "internal-integrations";
import { IngestEvent } from "../events/ingest.server";
+import { ManualWebhookSourceSchema } from "@trigger.dev/common-schemas";
+import { createHmac, timingSafeEqual } from "node:crypto";
+import { ulid } from "ulid";
type IgnoredEventResponse = {
status: "ignored";
@@ -41,16 +44,25 @@ export class HandleExternalSource {
async #createNormalizedRequest(request: Request): Promise
{
const requestUrl = new URL(request.url);
const rawSearchParams = requestUrl.searchParams;
- const rawBody = await request.json();
+ const rawBody = await request.text();
const rawHeaders = Object.fromEntries(request.headers.entries());
return {
- body: rawBody,
+ rawBody,
+ body: this.#safeJsonParse(rawBody),
headers: rawHeaders,
searchParams: rawSearchParams,
};
}
+ #safeJsonParse(json: string): any {
+ try {
+ return JSON.parse(json);
+ } catch (error) {
+ return null;
+ }
+ }
+
public async call(
externalSource: NonNullable,
serviceIdentifier: string,
@@ -123,6 +135,14 @@ export class HandleExternalSource {
serviceIdentifier: string,
request: NormalizedRequest
): Promise {
+ if (externalSource.manualRegistration) {
+ return this.#handleManualWebhook(
+ externalSource,
+ serviceIdentifier,
+ request
+ );
+ }
+
switch (serviceIdentifier) {
case "github": {
return github.webhooks.handleWebhookRequest({
@@ -137,4 +157,48 @@ export class HandleExternalSource {
}
}
}
+
+ async #handleManualWebhook(
+ externalSource: NonNullable,
+ serviceIdentifier: string,
+ request: NormalizedRequest
+ ): Promise {
+ const source = ManualWebhookSourceSchema.parse(externalSource.source);
+
+ if (source.verifyPayload.enabled && source.verifyPayload.header) {
+ const hmac = createHmac("sha256", externalSource.secret!);
+ const digest = Buffer.from(
+ hmac.update(request.rawBody).digest("hex"),
+ "utf8"
+ );
+
+ const providerSigString =
+ request.headers[source.verifyPayload.header.toLowerCase()] || "";
+
+ const providerSig = Buffer.from(providerSigString, "utf8");
+
+ if (
+ digest.length !== providerSig.length ||
+ !timingSafeEqual(digest, providerSig)
+ ) {
+ return {
+ status: "error",
+ error: "Payload signature did not match",
+ };
+ }
+ }
+
+ return {
+ status: "ok",
+ data: {
+ id: ulid(),
+ payload: request.body,
+ event: source.event,
+ context: {
+ headers: request.headers,
+ externalSourceId: externalSource.id,
+ },
+ },
+ };
+ }
}
diff --git a/apps/webapp/app/services/externalSources/registerExternalSource.server.ts b/apps/webapp/app/services/externalSources/registerExternalSource.server.ts
index 1b237a57d4d..66ea2ef53b8 100644
--- a/apps/webapp/app/services/externalSources/registerExternalSource.server.ts
+++ b/apps/webapp/app/services/externalSources/registerExternalSource.server.ts
@@ -5,7 +5,10 @@ import crypto from "node:crypto";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
-import { findExternalSourceById } from "~/models/externalSource.server";
+import {
+ buildExternalSourceUrl,
+ findExternalSourceById,
+} from "~/models/externalSource.server";
import { getAccessInfo } from "../accessInfo.server";
export class RegisterExternalSource {
@@ -26,6 +29,10 @@ export class RegisterExternalSource {
return true;
}
+ if (externalSource.manualRegistration) {
+ return true;
+ }
+
console.log("[RegisterExternalSource] registering external source", {
externalSource,
});
@@ -56,9 +63,13 @@ export class RegisterExternalSource {
throw new Error("No access token found for webhook");
}
- const secret = crypto.randomBytes(32).toString("hex");
+ const secret =
+ externalSource.secret ?? crypto.randomBytes(32).toString("hex");
- const webhookUrl = `${env.APP_ORIGIN}/api/v1/internal/webhooks/${connection.apiIdentifier}/${externalSource.id}`;
+ const webhookUrl = buildExternalSourceUrl(
+ externalSource,
+ connection.apiIdentifier
+ );
const serviceWebhook = await this.#registerWebhookWithConnection(
externalSource.service,
diff --git a/apps/webapp/app/services/fetches/performFetchRequest.server.ts b/apps/webapp/app/services/fetches/performFetchRequest.server.ts
index cb86b4d6af2..eaf147869c8 100644
--- a/apps/webapp/app/services/fetches/performFetchRequest.server.ts
+++ b/apps/webapp/app/services/fetches/performFetchRequest.server.ts
@@ -1,5 +1,5 @@
import type { FetchRequest } from ".prisma/client";
-import type { SecureString } from "@trigger.dev/common-schemas";
+import type { RetrySchema, SecureString } from "@trigger.dev/common-schemas";
import { FetchRequestSchema } from "@trigger.dev/common-schemas";
import type {
NormalizedResponse,
@@ -9,8 +9,6 @@ import type { z } from "zod";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
-const RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
-
type CallResponse =
| {
stop: true;
@@ -36,12 +34,28 @@ export class PerformFetchRequest {
return { stop: true };
}
- const performedRequest = await this.#performRequest(fetchRequest);
+ const request = FetchRequestSchema.parse(fetchRequest.fetch);
+
+ const retryConfig = {
+ enabled: true,
+ maxAttempts: 10,
+ minTimeout: 1000,
+ maxTimeout: 60000,
+ factor: 1.8,
+ statusCodes: [408, 429, 500, 502, 503, 504],
+ ...(request.retry ?? {}),
+ };
+
+ const performedRequest = await this.#performRequest(request, retryConfig);
if (performedRequest.ok) {
return this.#completeWithSuccess(fetchRequest, performedRequest.response);
} else if (performedRequest.isRetryable) {
- return this.#attemptRetry(fetchRequest, performedRequest.response);
+ return this.#attemptRetry(
+ retryConfig,
+ fetchRequest,
+ performedRequest.response
+ );
} else {
return this.#completeWithFailure(fetchRequest, performedRequest.response);
}
@@ -108,21 +122,11 @@ export class PerformFetchRequest {
}
async #attemptRetry(
+ retry: z.infer,
fetchRequest: FetchRequest,
response: NormalizedResponse
) {
- if (fetchRequest.retryCount >= 10) {
- await this.#prismaClient.fetchRequest.update({
- where: {
- id: fetchRequest.id,
- },
- data: {
- retryCount: {
- increment: 1,
- },
- },
- });
-
+ if (fetchRequest.retryCount >= retry.maxAttempts) {
return this.#completeWithFailure(fetchRequest, response);
}
@@ -143,7 +147,8 @@ export class PerformFetchRequest {
return {
stop: false as const,
retryInSeconds: this.#calculateRetryInSeconds(
- updatedFetchRequest.retryCount
+ updatedFetchRequest.retryCount,
+ retry
),
};
}
@@ -151,15 +156,11 @@ export class PerformFetchRequest {
// Exponential backoff with a configurable factor and a configurable maximum
#calculateRetryInSeconds(
retryCount: number,
- options: { factor: number; maxTimeout: number; minTimeout: number } = {
- factor: 1.8,
- minTimeout: 1000,
- maxTimeout: 60000,
- }
+ retryOptions: z.infer
) {
- const timeout = options.factor ** retryCount * options.minTimeout;
+ const timeout = retryOptions.factor ** retryCount * retryOptions.minTimeout;
- return Math.min(timeout, options.maxTimeout) / 1000;
+ return Math.min(timeout, retryOptions.maxTimeout) / 1000;
}
async #createResponse(
@@ -182,19 +183,36 @@ export class PerformFetchRequest {
}
async #performRequest(
- fetchRequest: FetchRequest
+ request: z.infer,
+ retry: z.infer
): Promise {
- const request = FetchRequestSchema.parse(fetchRequest.fetch);
- const requestInit = createFetchRequestInit(request);
-
- const response = await fetch(request.url, requestInit);
-
- const body = await this.#safeGetJson(response);
+ try {
+ const requestInit = createFetchRequestInit(request);
+
+ const response = await fetch(request.url, requestInit);
+
+ const body = await this.#safeGetJson(response);
+
+ if (response.ok) {
+ return {
+ ok: true,
+ isRetryable: false,
+ response: {
+ output: {
+ status: response.status,
+ headers: headersToRecord(response.headers),
+ body,
+ },
+ context: {},
+ },
+ };
+ }
- if (response.ok) {
+ // Only retry on retryable status codes
return {
- ok: true,
- isRetryable: false,
+ ok: false,
+ isRetryable:
+ retry.statusCodes.includes(response.status) && retry.enabled,
response: {
output: {
status: response.status,
@@ -204,21 +222,33 @@ export class PerformFetchRequest {
context: {},
},
};
+ } catch (error) {
+ if (error instanceof Error) {
+ return {
+ ok: false,
+ isRetryable: false,
+ response: {
+ output: {
+ name: error.name,
+ message: error.message,
+ },
+ context: {},
+ },
+ };
+ } else {
+ return {
+ ok: false,
+ isRetryable: false,
+ response: {
+ output: {
+ name: "UnknownError",
+ message: "Unknown error",
+ },
+ context: {},
+ },
+ };
+ }
}
-
- // Only retry on retryable status codes
- return {
- ok: false,
- isRetryable: RETRYABLE_STATUS_CODES.includes(response.status),
- response: {
- output: {
- status: response.status,
- headers: headersToRecord(response.headers),
- body,
- },
- context: {},
- },
- };
}
#safeGetJson = async (response: Response) => {
diff --git a/apps/webapp/app/services/workflows/registerWorkflow.server.ts b/apps/webapp/app/services/workflows/registerWorkflow.server.ts
index fb002be1b2d..d4b027d629c 100644
--- a/apps/webapp/app/services/workflows/registerWorkflow.server.ts
+++ b/apps/webapp/app/services/workflows/registerWorkflow.server.ts
@@ -1,6 +1,7 @@
import { github } from "internal-integrations";
import type { WorkflowMetadata } from "internal-platform";
import { WorkflowMetadataSchema } from "internal-platform";
+import crypto from "node:crypto";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import type { Organization } from "~/models/organization.server";
@@ -140,46 +141,14 @@ export class RegisterWorkflow {
) {
switch (payload.trigger.type) {
case "WEBHOOK": {
- if (!payload.trigger.source) {
- return;
- }
-
- const existingConnection =
- await this.#findLatestExistingConnectionInOrg(
- payload.trigger.service,
- organization
- );
-
- const externalSource = await this.#prismaClient.externalSource.upsert({
- where: {
- organizationId_key: {
- key: this.#keyForExternalSource(payload),
- organizationId: organization.id,
- },
- },
- update: {
- source: payload.trigger.source,
- },
- create: {
- organizationId: organization.id,
- key: this.#keyForExternalSource(payload),
- type: "WEBHOOK",
- source: payload.trigger.source,
- status: "CREATED",
- connectionId: existingConnection?.id,
- service: payload.trigger.service,
- },
- });
+ const externalSource = await this.#upsertWebhookSource(
+ payload,
+ organization,
+ workflow
+ );
- if (!externalSource.connectionId && existingConnection) {
- await this.#prismaClient.externalSource.update({
- where: {
- id: externalSource.id,
- },
- data: {
- connectionId: existingConnection.id,
- },
- });
+ if (!externalSource) {
+ return;
}
await this.#prismaClient.workflow.update({
@@ -235,6 +204,89 @@ export class RegisterWorkflow {
}
}
+ async #upsertWebhookSource(
+ payload: WorkflowMetadata,
+ organization: Organization,
+ workflow: Workflow
+ ) {
+ if (payload.trigger.type !== "WEBHOOK") {
+ return;
+ }
+
+ if (!payload.trigger.source) {
+ return;
+ }
+
+ const secret = crypto.randomBytes(16).toString("hex");
+
+ if (payload.trigger.manualRegistration) {
+ const externalSource = await this.#prismaClient.externalSource.upsert({
+ where: {
+ organizationId_key: {
+ key: `${workflow.id}-${payload.trigger.service}`,
+ organizationId: organization.id,
+ },
+ },
+ update: {
+ source: payload.trigger.source,
+ },
+ create: {
+ organizationId: organization.id,
+ key: `${workflow.id}-${payload.trigger.service}`,
+ type: "WEBHOOK",
+ source: payload.trigger.source,
+ status: "CREATED",
+ service: payload.trigger.service,
+ manualRegistration: true,
+ secret,
+ },
+ });
+
+ return externalSource;
+ } else {
+ const existingConnection = await this.#findLatestExistingConnectionInOrg(
+ payload.trigger.service,
+ organization
+ );
+
+ const externalSource = await this.#prismaClient.externalSource.upsert({
+ where: {
+ organizationId_key: {
+ key: this.#keyForExternalSource(payload),
+ organizationId: organization.id,
+ },
+ },
+ update: {
+ source: payload.trigger.source,
+ },
+ create: {
+ organizationId: organization.id,
+ key: this.#keyForExternalSource(payload),
+ type: "WEBHOOK",
+ source: payload.trigger.source,
+ status: "CREATED",
+ connectionId: existingConnection?.id,
+ service: payload.trigger.service,
+ manualRegistration: false,
+ secret,
+ },
+ });
+
+ if (!externalSource.connectionId && existingConnection) {
+ await this.#prismaClient.externalSource.update({
+ where: {
+ id: externalSource.id,
+ },
+ data: {
+ connectionId: existingConnection.id,
+ },
+ });
+ }
+
+ return externalSource;
+ }
+ }
+
#keyForExternalSource(payload: WorkflowMetadata): string {
if (payload.trigger.type === "WEBHOOK") {
switch (payload.trigger.service) {
diff --git a/apps/webapp/app/triggers/init.server.ts b/apps/webapp/app/triggers/init.server.ts
new file mode 100644
index 00000000000..f6ebb3d3642
--- /dev/null
+++ b/apps/webapp/app/triggers/init.server.ts
@@ -0,0 +1,17 @@
+import { uptimeCheck } from "./monitoring.server";
+
+declare global {
+ var __triggers_initialized: boolean;
+}
+
+export function init() {
+ if (global.__triggers_initialized) {
+ return;
+ }
+
+ global.__triggers_initialized = true;
+
+ uptimeCheck.listen();
+
+ console.log(`🛎 Triggers initialized`);
+}
diff --git a/apps/webapp/app/triggers/monitoring.server.ts b/apps/webapp/app/triggers/monitoring.server.ts
new file mode 100644
index 00000000000..01ff77c68b3
--- /dev/null
+++ b/apps/webapp/app/triggers/monitoring.server.ts
@@ -0,0 +1,26 @@
+import { Trigger, scheduleEvent } from "@trigger.dev/sdk";
+import { slack } from "@trigger.dev/integrations";
+import { prisma } from "~/db.server";
+
+export const uptimeCheck = new Trigger({
+ id: "uptime-check",
+ name: "Uptime Check",
+ on: scheduleEvent({ rateOf: { minutes: 1 } }),
+ triggerTTL: 300,
+ run: async (event, context) => {
+ // Grab counts of workflows, runs, and steps
+ const userCount = await prisma.user.count();
+ const workflowCount = await prisma.workflow.count();
+ const runCount = await prisma.workflowRun.count();
+ const stepCount = await prisma.workflowRunStep.count();
+
+ if (context.environment === "development") {
+ return;
+ }
+
+ await slack.postMessage("Uptime Notification", {
+ channelName: "monitoring",
+ text: `[${context.environment}] Uptime Check: ${userCount} users, ${workflowCount} workflows, ${runCount} runs, ${stepCount} steps.`,
+ });
+ },
+});
diff --git a/apps/webapp/package.json b/apps/webapp/package.json
index 282747443f3..8e3222ee25f 100644
--- a/apps/webapp/package.json
+++ b/apps/webapp/package.json
@@ -64,6 +64,9 @@
"@tanstack/react-table": "^8.0.0-alpha.87",
"@trigger.dev/common-schemas": "workspace:*",
"@trigger.dev/providers": "workspace:*",
+ "@trigger.dev/sdk": "workspace:*",
+ "@trigger.dev/integrations": "workspace:*",
+ "internal-bridge": "workspace:*",
"@uiw/react-codemirror": "^4.13.2",
"bcryptjs": "^2.4.3",
"classnames": "^2.3.1",
@@ -115,7 +118,8 @@
"tiny-invariant": "^1.2.0",
"tsx": "^3.4.3",
"ulid": "^2.3.0",
- "zod": "^3.20.2"
+ "zod": "^3.20.2",
+ "zod-error": "^1.1.0"
},
"devDependencies": {
"@faker-js/faker": "^7.5.0",
@@ -182,4 +186,4 @@
"engines": {
"node": ">=16.0.0"
}
-}
+}
\ No newline at end of file
diff --git a/apps/webapp/prisma/migrations/20230124224344_add_retry_to_fetch_request/migration.sql b/apps/webapp/prisma/migrations/20230124224344_add_retry_to_fetch_request/migration.sql
new file mode 100644
index 00000000000..adfd35d6529
--- /dev/null
+++ b/apps/webapp/prisma/migrations/20230124224344_add_retry_to_fetch_request/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "FetchRequest" ADD COLUMN "retry" JSONB;
diff --git a/apps/webapp/prisma/migrations/20230124224734_remove_retry/migration.sql b/apps/webapp/prisma/migrations/20230124224734_remove_retry/migration.sql
new file mode 100644
index 00000000000..10a891d6e48
--- /dev/null
+++ b/apps/webapp/prisma/migrations/20230124224734_remove_retry/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `retry` on the `FetchRequest` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "FetchRequest" DROP COLUMN "retry";
diff --git a/apps/webapp/prisma/migrations/20230125105732_add_manual_registration_to_external_source/migration.sql b/apps/webapp/prisma/migrations/20230125105732_add_manual_registration_to_external_source/migration.sql
new file mode 100644
index 00000000000..bf088658351
--- /dev/null
+++ b/apps/webapp/prisma/migrations/20230125105732_add_manual_registration_to_external_source/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "ExternalSource" ADD COLUMN "manualRegistration" BOOLEAN NOT NULL DEFAULT false;
diff --git a/apps/webapp/prisma/schema.prisma b/apps/webapp/prisma/schema.prisma
index ddb9fcce460..a3b0319619e 100644
--- a/apps/webapp/prisma/schema.prisma
+++ b/apps/webapp/prisma/schema.prisma
@@ -197,13 +197,14 @@ model ExternalSource {
service String
- workflows Workflow[]
- type ExternalSourceType
- key String
- source Json
- status ExternalSourceStatus @default(CREATED)
- externalData Json?
- secret String?
+ workflows Workflow[]
+ type ExternalSourceType
+ key String
+ source Json
+ status ExternalSourceStatus @default(CREATED)
+ externalData Json?
+ secret String?
+ manualRegistration Boolean @default(false)
readyAt DateTime?
createdAt DateTime @default(now())
diff --git a/apps/webapp/public/integrations/Intercom.png b/apps/webapp/public/integrations/Intercom.png
new file mode 100644
index 00000000000..9b5c1a26b28
Binary files /dev/null and b/apps/webapp/public/integrations/Intercom.png differ
diff --git a/apps/webapp/public/integrations/Twitter.png b/apps/webapp/public/integrations/Twitter.png
new file mode 100644
index 00000000000..dd631411286
Binary files /dev/null and b/apps/webapp/public/integrations/Twitter.png differ
diff --git a/apps/webapp/public/integrations/Webhook.png b/apps/webapp/public/integrations/Webhook.png
new file mode 100644
index 00000000000..83dade04ef8
Binary files /dev/null and b/apps/webapp/public/integrations/Webhook.png differ
diff --git a/apps/webapp/public/integrations/airtable.png b/apps/webapp/public/integrations/airtable.png
index c6340ff9353..1db66d919c4 100644
Binary files a/apps/webapp/public/integrations/airtable.png and b/apps/webapp/public/integrations/airtable.png differ
diff --git a/apps/webapp/public/integrations/asana.png b/apps/webapp/public/integrations/asana.png
new file mode 100644
index 00000000000..3df36914c85
Binary files /dev/null and b/apps/webapp/public/integrations/asana.png differ
diff --git a/apps/webapp/public/integrations/discord.png b/apps/webapp/public/integrations/discord.png
new file mode 100644
index 00000000000..f8cab81b3ff
Binary files /dev/null and b/apps/webapp/public/integrations/discord.png differ
diff --git a/apps/webapp/public/integrations/github.png b/apps/webapp/public/integrations/github.png
index e22a2ef3b06..f84accb814a 100644
Binary files a/apps/webapp/public/integrations/github.png and b/apps/webapp/public/integrations/github.png differ
diff --git a/apps/webapp/public/integrations/google-calendar.png b/apps/webapp/public/integrations/google-calendar.png
new file mode 100644
index 00000000000..ecea8a31462
Binary files /dev/null and b/apps/webapp/public/integrations/google-calendar.png differ
diff --git a/apps/webapp/public/integrations/google-docs.png b/apps/webapp/public/integrations/google-docs.png
new file mode 100644
index 00000000000..4cfaee96921
Binary files /dev/null and b/apps/webapp/public/integrations/google-docs.png differ
diff --git a/apps/webapp/public/integrations/google-sheets.png b/apps/webapp/public/integrations/google-sheets.png
new file mode 100644
index 00000000000..aea70fc8b3e
Binary files /dev/null and b/apps/webapp/public/integrations/google-sheets.png differ
diff --git a/apps/webapp/public/integrations/hubspot.png b/apps/webapp/public/integrations/hubspot.png
new file mode 100644
index 00000000000..9ac383c8b96
Binary files /dev/null and b/apps/webapp/public/integrations/hubspot.png differ
diff --git a/apps/webapp/public/integrations/jira.png b/apps/webapp/public/integrations/jira.png
new file mode 100644
index 00000000000..742d1f6456e
Binary files /dev/null and b/apps/webapp/public/integrations/jira.png differ
diff --git a/apps/webapp/public/integrations/linear.png b/apps/webapp/public/integrations/linear.png
new file mode 100644
index 00000000000..590185879c5
Binary files /dev/null and b/apps/webapp/public/integrations/linear.png differ
diff --git a/apps/webapp/public/integrations/pagerduty.png b/apps/webapp/public/integrations/pagerduty.png
new file mode 100644
index 00000000000..34432a0437d
Binary files /dev/null and b/apps/webapp/public/integrations/pagerduty.png differ
diff --git a/apps/webapp/public/integrations/posthog.png b/apps/webapp/public/integrations/posthog.png
new file mode 100644
index 00000000000..901d8991b09
Binary files /dev/null and b/apps/webapp/public/integrations/posthog.png differ
diff --git a/apps/webapp/public/integrations/resend.png b/apps/webapp/public/integrations/resend.png
index 22f076838ae..29e28c62030 100644
Binary files a/apps/webapp/public/integrations/resend.png and b/apps/webapp/public/integrations/resend.png differ
diff --git a/apps/webapp/public/integrations/salesforce.png b/apps/webapp/public/integrations/salesforce.png
new file mode 100644
index 00000000000..d4958e66a54
Binary files /dev/null and b/apps/webapp/public/integrations/salesforce.png differ
diff --git a/apps/webapp/public/integrations/slack.png b/apps/webapp/public/integrations/slack.png
index 4fe667121e9..82e8ae77a8d 100644
Binary files a/apps/webapp/public/integrations/slack.png and b/apps/webapp/public/integrations/slack.png differ
diff --git a/apps/webapp/public/integrations/todoist.png b/apps/webapp/public/integrations/todoist.png
new file mode 100644
index 00000000000..5ca0e581943
Binary files /dev/null and b/apps/webapp/public/integrations/todoist.png differ
diff --git a/apps/webapp/public/integrations/trello.png b/apps/webapp/public/integrations/trello.png
new file mode 100644
index 00000000000..fd829419bfd
Binary files /dev/null and b/apps/webapp/public/integrations/trello.png differ
diff --git a/apps/webapp/public/integrations/twilio.png b/apps/webapp/public/integrations/twilio.png
new file mode 100644
index 00000000000..384bf77d1ac
Binary files /dev/null and b/apps/webapp/public/integrations/twilio.png differ
diff --git a/apps/webapp/public/integrations/typeform.png b/apps/webapp/public/integrations/typeform.png
index 84f9fb5cd06..27c66e2897f 100644
Binary files a/apps/webapp/public/integrations/typeform.png and b/apps/webapp/public/integrations/typeform.png differ
diff --git a/apps/webapp/public/integrations/whatsapp.png b/apps/webapp/public/integrations/whatsapp.png
new file mode 100644
index 00000000000..01adff2cce5
Binary files /dev/null and b/apps/webapp/public/integrations/whatsapp.png differ
diff --git a/apps/webapp/public/integrations/zendesk.png b/apps/webapp/public/integrations/zendesk.png
new file mode 100644
index 00000000000..5a37032045b
Binary files /dev/null and b/apps/webapp/public/integrations/zendesk.png differ
diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js
index d6d8684e193..9861e581175 100644
--- a/apps/webapp/remix.config.js
+++ b/apps/webapp/remix.config.js
@@ -12,19 +12,26 @@ module.exports = {
"axios",
"internal-platform",
"internal-integrations",
+ "internal-bridge",
"@trigger.dev/providers",
"@trigger.dev/common-schemas",
+ "@trigger.dev/sdk",
+ "@trigger.dev/integrations",
"emails",
"internal-pulsar",
],
watchPaths: async () => {
return [
"../../packages/internal-platform/src/**/*",
+ "../../packages/internal-bridge/src/**/*",
"../../packages/common-schemas/src/**/*",
"../../packages/internal-integrations/src/**/*",
"../../packages/internal-providers/src/**/*",
"../../packages/emails/src/**/*",
"../../packages/internal-pulsar/src/**/*",
+ "../../packages/internal-platform/src/**/*",
+ "../../packages/trigger-sdk/src/**/*",
+ "../../packages/trigger-integrations/src/**/*",
];
},
};
diff --git a/apps/webapp/tsconfig.json b/apps/webapp/tsconfig.json
index 7adff1bff6f..eca32087416 100644
--- a/apps/webapp/tsconfig.json
+++ b/apps/webapp/tsconfig.json
@@ -24,6 +24,8 @@
"../../packages/internal-integrations/src/index"
],
"internal-integrations/*": ["../../packages/internal-integrations/src/*"],
+ "internal-bridge": ["../../packages/internal-bridge/src/index"],
+ "internal-bridge/*": ["../../packages/internal-bridge/src/*"],
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
"@trigger.dev/integrations": [
@@ -32,12 +34,8 @@
"@trigger.dev/integrations/*": [
"../../packages/trigger-integrations/src/*"
],
- "@trigger.dev/providers": [
- "../../packages/trigger-providers/src/index"
- ],
- "@trigger.dev/providers/*": [
- "../../packages/trigger-providers/src/*"
- ],
+ "@trigger.dev/providers": ["../../packages/trigger-providers/src/index"],
+ "@trigger.dev/providers/*": ["../../packages/trigger-providers/src/*"],
"@trigger.dev/common-schemas": [
"../../packages/common-schemas/src/index"
],
diff --git a/examples/fetch-playground/src/index.ts b/examples/fetch-playground/src/index.ts
index e54a082598b..9a2f2789f1d 100644
--- a/examples/fetch-playground/src/index.ts
+++ b/examples/fetch-playground/src/index.ts
@@ -1,9 +1,8 @@
-import { Trigger, customEvent } from "@trigger.dev/sdk";
+import { slack } from "@trigger.dev/integrations";
+import { Trigger, customEvent, webhookEvent } from "@trigger.dev/sdk";
import { z } from "zod";
-const TOKEN = "abc123";
-
new Trigger({
id: "fetch-playground",
name: "Fetch Playground",
@@ -20,6 +19,18 @@ new Trigger({
.default("GET"),
headers: z.record(z.string()).optional(),
body: z.any().optional(),
+ retry: z
+ .object({
+ enabled: z.boolean().default(true),
+ maxAttempts: z.number().default(3),
+ minTimeout: z.number().default(1000),
+ maxTimeout: z.number().default(60000),
+ factor: z.number().default(1.8),
+ statusCodes: z
+ .array(z.number())
+ .default([408, 429, 500, 502, 503, 504]),
+ })
+ .optional(),
}),
}),
run: async (event, ctx) => {
@@ -33,6 +44,7 @@ new Trigger({
responseSchema: z.any(),
headers: event.headers,
body: event.body ? JSON.stringify(event.body) : undefined,
+ retry: event.retry,
});
await ctx.logger.info("Received the fetch response", {
@@ -41,3 +53,106 @@ new Trigger({
});
},
}).listen();
+
+export const bookingPayloadSchema = z.object({
+ triggerEvent: z.string(),
+ createdAt: z.coerce.date(),
+ payload: z.object({
+ type: z.string(),
+ title: z.string(),
+ description: z.string(),
+ additionalNotes: z.string(),
+ customInputs: z.object({}),
+ startTime: z.coerce.date(),
+ endTime: z.coerce.date(),
+ organizer: z.object({
+ id: z.number(),
+ name: z.string(),
+ email: z.string(),
+ timeZone: z.string(),
+ language: z.object({ locale: z.string() }),
+ }),
+ attendees: z.array(
+ z.object({
+ email: z.string(),
+ name: z.string(),
+ timeZone: z.string(),
+ language: z.object({ locale: z.string() }),
+ })
+ ),
+ location: z.string(),
+ destinationCalendar: z.object({
+ id: z.number(),
+ integration: z.string(),
+ externalId: z.string(),
+ userId: z.number(),
+ eventTypeId: z.null(),
+ credentialId: z.number(),
+ }),
+ hideCalendarNotes: z.boolean(),
+ requiresConfirmation: z.null(),
+ eventTypeId: z.number(),
+ seatsShowAttendees: z.boolean(),
+ uid: z.string(),
+ conferenceData: z.object({
+ createRequest: z.object({ requestId: z.string() }),
+ }),
+ videoCallData: z.object({
+ type: z.string(),
+ id: z.string(),
+ password: z.string(),
+ url: z.string(),
+ }),
+ appsStatus: z.array(
+ z.object({
+ appName: z.string(),
+ type: z.string(),
+ success: z.number(),
+ failures: z.number(),
+ errors: z.array(z.any()).optional(),
+ warnings: z.array(z.any()).optional(),
+ })
+ ),
+ eventTitle: z.string(),
+ eventDescription: z.null(),
+ price: z.number(),
+ currency: z.string(),
+ length: z.number(),
+ bookingId: z.number(),
+ metadata: z.object({ videoCallUrl: z.string() }),
+ status: z.string(),
+ }),
+});
+
+new Trigger({
+ id: "caldotcom-to-slack-2",
+ name: "Cal.com To Slack",
+ apiKey: "trigger_dev_zC25mKNn6c0q",
+ endpoint: "ws://localhost:8889/ws",
+ logLevel: "debug",
+ on: webhookEvent({
+ service: "cal.com",
+ eventName: "BOOKING_CREATED",
+ filter: {
+ triggerEvent: ["BOOKING_CREATED"],
+ },
+ schema: bookingPayloadSchema,
+ verifyPayload: {
+ enabled: true,
+ header: "X-Cal-Signature-256",
+ },
+ }),
+ run: async (event, ctx) => {
+ await ctx.logger.info("Received a cal.com booking", {
+ event,
+ wallTime: new Date(),
+ });
+
+ await slack.postMessage(`Cal.com booking yo`, {
+ channelName: "customers",
+ text: `New Booking: ${
+ event.payload.title
+ } at ${event.payload.startTime.toLocaleDateString()}`,
+ });
+ },
+}).listen();
diff --git a/flightcontrol.json b/flightcontrol.json
index 2e3102f414f..c5f096a2e11 100644
--- a/flightcontrol.json
+++ b/flightcontrol.json
@@ -62,10 +62,11 @@
"packages/internal-pulsar/src/**",
"./pnpm-lock.yaml"
],
- "dependsOn": [
- "p-db"
- ],
+ "dependsOn": ["p-db"],
"envVariables": {
+ "TRIGGER_API_KEY": {
+ "fromParameterStore": "/Prod/webapp/TRIGGER_API_KEY"
+ },
"FROM_EMAIL": "hello@email.trigger.dev",
"REPLY_TO_EMAIL": "hello@trigger.dev",
"RESEND_API_KEY": {
@@ -103,6 +104,9 @@
"PIZZLY_SECRET_KEY": {
"fromParameterStore": "/Prod/webapp/PIZZLY_SECRET_KEY"
},
+ "POSTHOG_PROJECT_KEY": {
+ "fromParameterStore": "/Prod/webapp/POSTHOG_PROJECT_KEY"
+ },
"TRIGGER_LOG_LEVEL": "debug",
"PULSAR_ENABLED": "1",
"PULSAR_DEBUG": true
@@ -234,9 +238,7 @@
"packages/internal-pulsar/src/**",
"./pnpm-lock.yaml"
],
- "dependsOn": [
- "s-db"
- ],
+ "dependsOn": ["s-db"],
"envVariables": {
"FROM_EMAIL": "hello@email.trigger.dev",
"REPLY_TO_EMAIL": "hello@trigger.dev",
@@ -346,4 +348,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/packages/common-schemas/src/events.ts b/packages/common-schemas/src/events.ts
index 80b1fe66db2..26805d54492 100644
--- a/packages/common-schemas/src/events.ts
+++ b/packages/common-schemas/src/events.ts
@@ -62,3 +62,11 @@ export const ScheduleSourceSchema = z.union([
]);
export type ScheduleSource = z.infer;
+
+export const ManualWebhookSourceSchema = z.object({
+ verifyPayload: z.object({
+ enabled: z.boolean(),
+ header: z.string().optional(),
+ }),
+ event: z.string(),
+});
diff --git a/packages/common-schemas/src/fetch.ts b/packages/common-schemas/src/fetch.ts
index dab1dd770a9..1a8bf56bc93 100644
--- a/packages/common-schemas/src/fetch.ts
+++ b/packages/common-schemas/src/fetch.ts
@@ -8,6 +8,15 @@ export const SecureStringSchema = z.object({
export type SecureString = z.infer;
+export const RetrySchema = z.object({
+ enabled: z.boolean().default(true),
+ factor: z.number().default(1.8),
+ maxTimeout: z.number().default(60000),
+ minTimeout: z.number().default(1000),
+ maxAttempts: z.number().default(10),
+ statusCodes: z.array(z.number()).default([408, 429, 500, 502, 503, 504]),
+});
+
export const FetchRequestSchema = z.object({
url: z.string(),
headers: z.record(z.union([z.string(), SecureStringSchema])).optional(),
@@ -22,6 +31,7 @@ export const FetchRequestSchema = z.object({
"TRACE",
]),
body: z.any(),
+ retry: RetrySchema.optional(),
});
export const FetchOutputSchema = z.object({
diff --git a/packages/common-schemas/src/triggers.ts b/packages/common-schemas/src/triggers.ts
index 81611d59172..539adffa4fe 100644
--- a/packages/common-schemas/src/triggers.ts
+++ b/packages/common-schemas/src/triggers.ts
@@ -15,7 +15,8 @@ export const WebhookEventTriggerSchema = z.object({
service: z.string(),
name: z.string(),
filter: EventFilterSchema,
- source: JsonSchema,
+ source: JsonSchema.optional(),
+ manualRegistration: z.boolean().default(false),
});
export type WebhookEventTrigger = z.infer;
diff --git a/packages/internal-bridge/src/schemas/server.ts b/packages/internal-bridge/src/schemas/server.ts
index 3d6d27bc6a1..4f86e838e9d 100644
--- a/packages/internal-bridge/src/schemas/server.ts
+++ b/packages/internal-bridge/src/schemas/server.ts
@@ -1,6 +1,7 @@
import {
CustomEventSchema,
FetchRequestSchema,
+ RetrySchema,
TriggerMetadataSchema,
WaitSchema,
} from "@trigger.dev/common-schemas";
diff --git a/packages/internal-bridge/src/zodRPC.ts b/packages/internal-bridge/src/zodRPC.ts
index 96cf468686d..0da7f147a65 100644
--- a/packages/internal-bridge/src/zodRPC.ts
+++ b/packages/internal-bridge/src/zodRPC.ts
@@ -117,8 +117,10 @@ export class ZodRPC<
public send(
key: K,
- data: z.infer
+ data: z.input
) {
+ this.#logger.debug("Sending call", { key, data });
+
const id = generateStableId(this.#connection.id, key as string, data);
const message = packageMessage({ id, methodName: key as string, data });
diff --git a/packages/internal-integrations/src/types.ts b/packages/internal-integrations/src/types.ts
index 40218736881..c9006a3159b 100644
--- a/packages/internal-integrations/src/types.ts
+++ b/packages/internal-integrations/src/types.ts
@@ -13,6 +13,7 @@ export interface WebhookConfig {
}
export interface NormalizedRequest {
+ rawBody: string;
body: any;
headers: Record;
searchParams: URLSearchParams;
diff --git a/packages/internal-platform/src/messages/schemas/fetchRequests.ts b/packages/internal-platform/src/messages/schemas/fetchRequests.ts
index b60d1c07b3a..7e1a354240e 100644
--- a/packages/internal-platform/src/messages/schemas/fetchRequests.ts
+++ b/packages/internal-platform/src/messages/schemas/fetchRequests.ts
@@ -2,6 +2,7 @@ import {
FetchOutputSchema,
FetchRequestSchema,
JsonSchema,
+ RetrySchema,
} from "@trigger.dev/common-schemas";
import { z } from "zod";
import {
diff --git a/packages/trigger-integrations/src/integrations/github/events.ts b/packages/trigger-integrations/src/integrations/github/events.ts
index 985d5c28e98..1ce94e1e861 100644
--- a/packages/trigger-integrations/src/integrations/github/events.ts
+++ b/packages/trigger-integrations/src/integrations/github/events.ts
@@ -26,6 +26,7 @@ export function commitCommentEvent(params: {
repo: params.repo,
events: ["commit_comment"],
}),
+ manualRegistration: false,
},
schema: github.schemas.commitComments.commitCommentEventSchema,
};
@@ -54,6 +55,7 @@ export function issueEvent(params: {
repo: params.repo,
events: ["issues"],
}),
+ manualRegistration: false,
},
schema: github.schemas.issues.issuesEventSchema,
};
@@ -82,6 +84,7 @@ export function issueCommentEvent(params: {
repo: params.repo,
events: ["issue_comment"],
}),
+ manualRegistration: false,
},
schema: github.schemas.issuesComments.issueCommentEventSchema,
};
@@ -110,6 +113,7 @@ export function pullRequestEvent(params: {
repo: params.repo,
events: ["pull_request"],
}),
+ manualRegistration: false,
},
schema: github.schemas.pullRequest.pullRequestEventSchema,
};
@@ -140,6 +144,7 @@ export function pullRequestCommentEvent(params: {
repo: params.repo,
events: ["pull_request_review_comment"],
}),
+ manualRegistration: false,
},
schema:
github.schemas.pullRequestComments.pullRequestReviewCommentEventSchema,
@@ -171,6 +176,7 @@ export function pullRequestReviewEvent(params: {
repo: params.repo,
events: ["pull_request_review"],
}),
+ manualRegistration: false,
},
schema: github.schemas.pullRequestReviews.pullRequestReviewEventSchema,
};
@@ -199,6 +205,7 @@ export function pushEvent(params: {
repo: params.repo,
events: ["push"],
}),
+ manualRegistration: false,
},
schema: github.schemas.push.pushEventSchema,
};
@@ -228,6 +235,7 @@ export function newStarEvent(params: {
repo: params.repo,
events: ["star"],
}),
+ manualRegistration: false,
},
schema: github.schemas.stars.starCreatedEventSchema,
};
diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json
index b44973134e5..786daf09e33 100644
--- a/packages/trigger-sdk/package.json
+++ b/packages/trigger-sdk/package.json
@@ -23,6 +23,7 @@
"@types/debug": "^4.1.7",
"@types/node": "16",
"@types/node-fetch": "2.6.x",
+ "@types/slug": "^5.0.3",
"@types/uuid": "^9.0.0",
"@types/ws": "^8.5.3",
"internal-bridge": "workspace:*",
@@ -39,6 +40,7 @@
"debug": "^4.3.4",
"evt": "^2.4.13",
"node-fetch": "2.6.x",
+ "slug": "^6.0.0",
"ulid": "^2.3.0",
"uuid": "^9.0.0",
"ws": "^8.11.0",
diff --git a/packages/trigger-sdk/src/client.ts b/packages/trigger-sdk/src/client.ts
index b126dce12de..ae181af679f 100644
--- a/packages/trigger-sdk/src/client.ts
+++ b/packages/trigger-sdk/src/client.ts
@@ -335,6 +335,7 @@ export class TriggerClient {
method: options.method ?? "GET",
headers: options.headers,
body: options.body,
+ retry: options.retry,
},
timestamp: String(highPrecisionTimestamp()),
});
@@ -508,7 +509,14 @@ export class TriggerClient {
};
}
- console.error(anyError);
+ const parsedError = z
+ .object({ name: z.string(), message: z.string() })
+ .passthrough()
+ .safeParse(error);
+
+ if (parsedError.success) {
+ return parsedError.data;
+ }
return {
name: "UnknownError",
diff --git a/packages/trigger-sdk/src/events.ts b/packages/trigger-sdk/src/events.ts
index f0659c2e86d..5cf8b3cab4b 100644
--- a/packages/trigger-sdk/src/events.ts
+++ b/packages/trigger-sdk/src/events.ts
@@ -3,8 +3,10 @@ import {
TriggerMetadataSchema,
ScheduleSourceSchema,
ScheduledEventPayloadSchema,
+ EventFilter,
} from "@trigger.dev/common-schemas";
import { z } from "zod";
+import slugify from "slug";
export type EventRule = z.infer;
@@ -16,6 +18,7 @@ export type TriggerEvent = {
export type TriggerCustomEventOptions = {
name: string;
schema: TSchema;
+ filter?: EventFilter;
};
export function customEvent(
@@ -26,7 +29,7 @@ export function customEvent(
type: "CUSTOM_EVENT",
service: "trigger",
name: options.name,
- filter: { event: [options.name] },
+ filter: { event: [options.name], payload: options.filter ?? {} },
},
schema: options.schema,
};
@@ -47,3 +50,37 @@ export function scheduleEvent(
schema: ScheduledEventPayloadSchema,
};
}
+
+export type TriggerWebhookEventOptions = {
+ schema: TSchema;
+ service: string;
+ eventName: string;
+ filter?: EventFilter;
+ verifyPayload?: {
+ enabled: boolean;
+ header: string;
+ };
+};
+
+export function webhookEvent(
+ options: TriggerWebhookEventOptions
+): TriggerEvent {
+ return {
+ metadata: {
+ type: "WEBHOOK",
+ service: slugify(options.service),
+ name: options.eventName,
+ filter: {
+ service: [slugify(options.service)],
+ payload: options.filter ?? {},
+ event: [options.eventName],
+ },
+ source: {
+ verifyPayload: options.verifyPayload ?? { enabled: false },
+ event: options.eventName,
+ },
+ manualRegistration: true,
+ },
+ schema: options.schema,
+ };
+}
diff --git a/packages/trigger-sdk/src/localStorage.ts b/packages/trigger-sdk/src/localStorage.ts
index 4f3de2402b6..2a61f417719 100644
--- a/packages/trigger-sdk/src/localStorage.ts
+++ b/packages/trigger-sdk/src/localStorage.ts
@@ -1,11 +1,6 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { z } from "zod";
-import {
- FetchOptions,
- FetchResponse,
- TriggerCustomEvent,
- TriggerFetch,
-} from "./types";
+import { TriggerCustomEvent, TriggerFetch } from "./types";
type PerformRequestOptions = {
service: string;
diff --git a/packages/trigger-sdk/src/types.ts b/packages/trigger-sdk/src/types.ts
index 8089c10e088..075d94e717d 100644
--- a/packages/trigger-sdk/src/types.ts
+++ b/packages/trigger-sdk/src/types.ts
@@ -31,6 +31,14 @@ export type FetchOptions<
body?: z.infer;
headers?: Record;
responseSchema?: TResponseBodySchema;
+ retry?: {
+ enabled?: boolean;
+ factor?: number;
+ maxTimeout?: number;
+ minTimeout?: number;
+ maxAttempts?: number;
+ statusCodes?: number[];
+ };
};
export type FetchResponse<
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ba2fc009ae1..99fa5003653 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,9 +31,9 @@ importers:
apps/docs:
specifiers:
- mintlify: ^2.0.14
+ mintlify: ^2.0.15
devDependencies:
- mintlify: 2.0.14
+ mintlify: 2.0.15
apps/webapp:
specifiers:
@@ -75,7 +75,9 @@ importers:
'@testing-library/react': ^13.4.0
'@testing-library/user-event': ^14.4.3
'@trigger.dev/common-schemas': workspace:*
+ '@trigger.dev/integrations': workspace:*
'@trigger.dev/providers': workspace:*
+ '@trigger.dev/sdk': workspace:*
'@trigger.dev/tailwind-config': '*'
'@types/bcryptjs': ^2.4.2
'@types/compression': ^1.7.2
@@ -122,6 +124,7 @@ importers:
glob: ^8.0.3
happy-dom: ^6.0.4
humanize-duration: ^3.27.3
+ internal-bridge: workspace:*
internal-integrations: workspace:*
internal-platform: workspace:*
internal-pulsar: workspace:*
@@ -176,6 +179,7 @@ importers:
vite-tsconfig-paths: ^3.5.1
vitest: ^0.23.4
zod: ^3.20.2
+ zod-error: ^1.1.0
dependencies:
'@aws-sdk/client-s3': 3.245.0
'@aws-sdk/s3-request-presigner': 3.245.0
@@ -204,7 +208,9 @@ importers:
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
'@tanstack/react-table': 8.7.6_biqbaboplfbrettd7655fr4n2y
'@trigger.dev/common-schemas': link:../../packages/common-schemas
+ '@trigger.dev/integrations': link:../../packages/trigger-integrations
'@trigger.dev/providers': link:../../packages/trigger-providers
+ '@trigger.dev/sdk': link:../../packages/trigger-sdk
'@uiw/react-codemirror': 4.19.5_aguurb4bmecpxzejz52amioxne
bcryptjs: 2.4.3
classnames: 2.3.2
@@ -219,6 +225,7 @@ importers:
emails: link:../../packages/emails
express: 4.18.2
humanize-duration: 3.27.3
+ internal-bridge: link:../../packages/internal-bridge
internal-integrations: link:../../packages/internal-integrations
internal-platform: link:../../packages/internal-platform
internal-pulsar: link:../../packages/internal-pulsar
@@ -257,6 +264,7 @@ importers:
tsx: 3.12.2
ulid: 2.3.0
zod: 3.20.2
+ zod-error: 1.1.0
devDependencies:
'@faker-js/faker': 7.6.0
'@remix-run/dev': 1.10.0_biqbaboplfbrettd7655fr4n2y
@@ -810,6 +818,7 @@ importers:
'@types/debug': ^4.1.7
'@types/node': '16'
'@types/node-fetch': 2.6.x
+ '@types/slug': ^5.0.3
'@types/uuid': ^9.0.0
'@types/ws': ^8.5.3
debug: ^4.3.4
@@ -817,6 +826,7 @@ importers:
internal-bridge: workspace:*
node-fetch: 2.6.x
rimraf: ^3.0.2
+ slug: ^6.0.0
tsup: ^6.5.0
tsx: ^3.12.1
ulid: ^2.3.0
@@ -828,6 +838,7 @@ importers:
debug: 4.3.4
evt: 2.4.13
node-fetch: 2.6.7
+ slug: 6.1.0
ulid: 2.3.0
uuid: 9.0.0
ws: 8.12.0
@@ -839,6 +850,7 @@ importers:
'@types/debug': 4.1.7
'@types/node': 16.18.11
'@types/node-fetch': 2.6.2
+ '@types/slug': 5.0.3
'@types/uuid': 9.0.0
'@types/ws': 8.5.4
internal-bridge: link:../internal-bridge
@@ -6521,7 +6533,7 @@ packages:
/axios/0.25.0_debug@4.3.4:
resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==}
dependencies:
- follow-redirects: 1.15.2
+ follow-redirects: 1.15.2_debug@4.3.4
transitivePeerDependencies:
- debug
dev: true
@@ -6529,7 +6541,7 @@ packages:
/axios/0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
dependencies:
- follow-redirects: 1.15.2
+ follow-redirects: 1.15.2_debug@4.3.4
form-data: 4.0.0
transitivePeerDependencies:
- debug
@@ -6538,7 +6550,7 @@ packages:
/axios/1.2.2:
resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==}
dependencies:
- follow-redirects: 1.15.2
+ follow-redirects: 1.15.2_debug@4.3.4
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
@@ -9637,7 +9649,7 @@ packages:
engines: {node: '>=0.4.0'}
dev: true
- /follow-redirects/1.15.2:
+ /follow-redirects/1.15.2_debug@4.3.4:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
@@ -9645,6 +9657,8 @@ packages:
peerDependenciesMeta:
debug:
optional: true
+ dependencies:
+ debug: 4.3.4
/for-each/0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -9753,6 +9767,15 @@ packages:
universalify: 2.0.0
dev: true
+ /fs-extra/11.1.0:
+ resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==}
+ engines: {node: '>=14.14'}
+ dependencies:
+ graceful-fs: 4.2.10
+ jsonfile: 6.1.0
+ universalify: 2.0.0
+ dev: true
+
/fs-extra/7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
@@ -12514,8 +12537,8 @@ packages:
yallist: 4.0.0
dev: true
- /mintlify/2.0.14:
- resolution: {integrity: sha512-Xe6lE5w5hJKW70s1J+//CUzpH2QMhKF4sHMbxeA4baBIrtyQGdWfgeFjLaZrgXS/ghkQ16NU0Qtqs4EdilgtJg==}
+ /mintlify/2.0.15:
+ resolution: {integrity: sha512-GM/rXnEJunAVRsHbk/3utKgF0BO7l9wj6z3VH0Csp18sRKlb7GjucxeVzUX+jXPoKC+t9JX02WbQg28MHJ/etA==}
engines: {node: '>=18.0.0'}
hasBin: true
dependencies:
@@ -12527,7 +12550,7 @@ packages:
cheerio: 0.22.0
chokidar: 3.5.3
favicons: 7.0.2
- fs-extra: 10.1.0
+ fs-extra: 11.1.0
gray-matter: 4.0.3
inquirer: 9.1.4
is-absolute-url: 4.0.1