Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/hosted-webhook-ingress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@trigger.dev/core": minor
"@trigger.dev/sdk": minor
"@trigger.dev/slack": minor
"trigger.dev": minor
---

Add hosted webhooks: receive and verify provider webhooks as a task, with no ingress or verification code of your own.

- `webhook()` declares an endpoint that routes a verified, typed event to an `onEvent` handler. Choose a source with a preset (`webhooks.stripe()`, `webhooks.github()`, and others) or `webhooks.custom<T>(config)`. Declared webhooks are discovered like tasks and synced to a hosted URL on deploy.
- `filter` gates which deliveries run, using a type-safe expression checked against the event at author time (`event.`/`header.`/`webhook.` paths, `&&`/`||`, comparison and `in`/`contains` operators, field-to-field comparison, and array quantifiers). A non-matching delivery is still recorded, not routed.
- `chat.event({ source, key, type })` routes deliveries that share a `key` to one durable session (per customer, installation, or issue) and delivers them to an agent's `onAction` as a typed envelope.
- Channels turn a chat surface into an agent frontend: `chat.channels.custom({ source, key, inbound, send })`, or the new `@trigger.dev/slack` package's `slack()` (Slack Events API verification, per-thread sessions, `chat.postMessage`/`chat.update` egress, `mentions()`, `startOn`, lifecycle reactions). Inbound messages run as turns and the reply posts back. Human-in-the-loop is built in: a tool with no `execute` pauses the turn, the connector posts controls (Slack ships Approve / Deny buttons), and a verified click resolves the tool and resumes the run.
- HTTP API for listing webhook endpoints and deliveries, plus rotate-secret, enable/disable, and replay.
14 changes: 9 additions & 5 deletions apps/webapp/app/assets/icons/WebhookIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
export function WebhookIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 190 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="7" r="1.75" fill="currentColor" />
<circle cx="7" cy="16" r="1.75" fill="currentColor" />
<circle cx="17" cy="16" r="1.75" fill="currentColor" />
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M97.1775 62.8673C96.7903 62.892 96.4025 62.9043 96.0145 62.9042C86.0767 62.9042 78.008 54.8167 78.008 44.8556C78.008 34.8946 86.0767 26.807 96.0145 26.807C105.952 26.807 114.021 34.8946 114.021 44.8556C114.027 48.9969 112.605 53.0129 109.996 56.2245L129.9 92.8984C134.805 91.1113 139.984 90.1972 145.203 90.1972C169.928 90.1972 190 110.317 190 135.099C190 159.881 169.928 180 145.203 180C136.454 179.997 127.896 177.435 120.577 172.628C119.792 172.117 119.128 171.439 118.632 170.642C118.136 169.846 117.82 168.95 117.706 168.019C117.593 167.087 117.684 166.141 117.974 165.249C118.263 164.356 118.745 163.538 119.384 162.851C119.397 162.837 119.411 162.822 119.437 162.819C120.511 161.665 121.955 160.925 123.518 160.727C125.08 160.53 126.662 160.888 127.989 161.738C133.116 165.067 139.095 166.838 145.203 166.838C162.68 166.838 176.868 152.616 176.868 135.099C176.868 117.581 162.68 103.36 145.203 103.36C139.151 103.36 130.562 106.013 125.398 109.298C125.132 109.498 124.826 109.639 124.502 109.71C124.177 109.781 123.841 109.781 123.516 109.711C123.191 109.64 122.885 109.5 122.619 109.301C122.352 109.101 122.132 108.846 121.973 108.554L97.1775 62.8673ZM88.9103 140.885C88.0018 146.036 86.2022 150.988 83.5926 155.517C71.2305 176.98 43.8113 184.345 22.3993 171.954C0.987294 159.563 -6.35938 132.079 6.0019 110.616C10.3798 103.023 16.8727 96.8744 24.6848 92.9248C25.5195 92.4994 26.4369 92.2623 27.3727 92.2302C28.3085 92.1981 29.24 92.3718 30.1017 92.739C30.9639 93.1059 31.7354 93.6575 32.3621 94.3552C32.9888 95.0529 33.4555 95.8797 33.7294 96.7775C33.7346 96.7969 33.7399 96.8153 33.7294 96.8391C34.1899 98.3489 34.1078 99.9727 33.4973 101.428C32.8868 102.883 31.7866 104.078 30.3881 104.804C24.9482 107.59 20.4286 111.894 17.3741 117.197C8.63613 132.367 13.829 151.795 28.9642 160.554C44.0994 169.313 63.4824 164.107 72.2204 148.936C75.2464 143.683 77.2482 134.9 76.9926 128.775C76.9519 128.444 76.9829 128.108 77.0835 127.79C77.1841 127.473 77.3519 127.18 77.5754 126.934C77.799 126.687 78.073 126.491 78.3788 126.36C78.6846 126.229 79.015 126.166 79.3475 126.175L131.219 127.494C131.39 127.148 131.573 126.805 131.769 126.466C136.737 117.84 147.759 114.879 156.366 119.859C164.974 124.841 167.926 135.888 162.957 144.515C157.988 153.141 146.966 156.101 138.359 151.12C134.778 149.055 132.019 145.813 130.548 141.943L88.9103 140.885ZM67.1689 79.2862C63.1721 75.9215 59.7926 71.8828 57.1827 67.3521C44.8205 45.8901 52.1672 18.4061 73.5792 6.01594C94.9912 -6.3742 122.411 0.98963 134.773 22.4507C139.144 30.0472 141.21 38.757 140.717 47.5127C140.667 48.4501 140.414 49.3652 139.973 50.1937C139.533 51.0222 138.917 51.7442 138.169 52.3088C137.421 52.8734 136.559 53.2671 135.643 53.4622C134.727 53.6572 133.779 53.649 132.866 53.438C132.847 53.4336 132.828 53.4283 132.812 53.4072C131.278 53.0521 129.916 52.169 128.964 50.9115C128.012 49.654 127.53 48.1018 127.602 46.5249C127.915 40.4094 126.456 34.3338 123.4 29.031C114.662 13.8614 95.2802 8.65544 80.145 17.4139C65.0107 26.1731 59.8161 45.6005 68.5549 60.771C71.5809 66.0244 78.1687 72.1539 83.5882 74.9942C83.8943 75.1245 84.1687 75.3194 84.3928 75.5657C84.617 75.8119 84.7855 76.1036 84.8871 76.421C84.9886 76.7384 85.0207 77.074 84.9812 77.405C84.9417 77.7359 84.8315 78.0544 84.6581 78.3389L57.5815 122.706C57.7958 123.027 58.0005 123.357 58.1963 123.696C63.1644 132.324 60.2113 143.371 51.6042 148.352C42.9979 153.332 31.9762 150.372 27.0072 141.745C22.0383 133.119 24.9914 122.071 33.5976 117.091C37.173 115.015 41.3538 114.241 45.4328 114.9L67.1689 79.2862Z"
d="M16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 8.14562 8.48161 9.17875 9.25341 9.90798C9.65459 10.287 9.83991 10.8882 9.57187 11.3706L8.94292 12.5027L7 16M12 7L13.9429 10.4973L14.571 11.6278C14.8394 12.1109 15.4487 12.2704 15.9833 12.1304C16.3079 12.0453 16.6487 12 17 12C19.2091 12 21 13.7909 21 16C21 18.2091 19.2091 20 17 20C16.2949 20 15.6323 19.8175 15.0571 19.4973M17 16H12C11.4477 16 11.0128 16.4547 10.8766 16.9899C10.4361 18.7202 8.86748 20 7 20C4.79086 20 3 18.2091 3 16C3 14.496 3.83007 13.1859 5.05708 12.5027"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
Expand Down
25 changes: 25 additions & 0 deletions apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ExclamationTriangleIcon,
} from "@heroicons/react/24/outline";
import { useFetcher, useNavigation, useSubmit } from "@remix-run/react";
import { WebhookIcon } from "~/assets/icons/WebhookIcon";
import { LayoutGroup, motion } from "framer-motion";
import {
type CSSProperties,
Expand Down Expand Up @@ -119,6 +120,7 @@ import {
v3SessionsPath,
v3UsagePath,
v3WaitpointTokensPath,
v3WebhooksPath,
} from "~/utils/pathBuilder";
import { FreePlanUsage } from "../billing/FreePlanUsage";
import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence";
Expand Down Expand Up @@ -920,6 +922,29 @@ export function SideMenu({
</SideMenuSection>
)}

{(user.admin || user.isImpersonating || featureFlags.hasWebhooksAccess) && (
<SideMenuSection
title="Webhooks"
isSideMenuCollapsed={isCollapsed}
itemSpacingClassName="space-y-0"
initialCollapsed={getSectionCollapsed(
user.dashboardPreferences.sideMenu,
"webhooks"
)}
onCollapseToggle={handleSectionToggle("webhooks")}
>
<SideMenuItem
name="Deliveries"
icon={WebhookIcon}
activeIconColor="text-webhooks"
inactiveIconColor="text-text-dimmed"
to={v3WebhooksPath(organization, project, environment)}
data-action="webhook-deliveries"
isCollapsed={isCollapsed}
/>
</SideMenuSection>
)}

<SideMenuSection
title="Deployments"
isSideMenuCollapsed={isCollapsed}
Expand Down
1 change: 1 addition & 0 deletions apps/webapp/app/components/navigation/sideMenuTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const SideMenuSectionIdSchema = z.enum([
"deployments",
"project-settings",
"tasks",
"webhooks",
]);

// Inferred type from the schema
Expand Down
6 changes: 3 additions & 3 deletions apps/webapp/app/components/run/RunTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import { LiveTimer } from "../runs/v3/LiveTimer";
// Types for the RunTimeline component
export type TimelineEventState = "complete" | "error" | "inprogress" | "delayed";

type TimelineLineVariant = "light" | "normal";
export type TimelineLineVariant = "light" | "normal";

type TimelineStyle = "normal" | "diminished";

type TimelineEventVariant =
export type TimelineEventVariant =
| "start-cap"
| "dot-hollow"
| "dot-solid"
Expand Down Expand Up @@ -323,7 +323,7 @@ function buildTimelineItems(run: TimelineSpanRun): TimelineItem[] {
export type RunTimelineEventProps = {
title: ReactNode;
subtitle?: ReactNode;
state?: "complete" | "error" | "inprogress";
state?: TimelineEventState;
variant?: TimelineEventVariant;
helpText?: string;
style?: TimelineStyle;
Expand Down
3 changes: 2 additions & 1 deletion apps/webapp/app/components/runs/v3/RunFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export const TaskRunListSearchFilters = z.object({
),
errorId: z.string().optional().describe("Error ID to filter runs by (e.g. error_abc123)"),
sources: StringOrStringArray.describe(
"Task trigger sources to filter by (STANDARD, SCHEDULED, AGENT)"
"Task trigger sources to filter by (STANDARD, SCHEDULED, AGENT, WEBHOOK)"
),
});

Expand Down Expand Up @@ -1918,6 +1918,7 @@ const sourceOptions: { value: TaskTriggerSource; title: string }[] = [
{ value: "STANDARD", title: "Standard" },
{ value: "SCHEDULED", title: "Scheduled" },
{ value: "AGENT", title: "Agent" },
{ value: "WEBHOOK", title: "Webhook" },
];

function SourceDropdown({
Expand Down
9 changes: 9 additions & 0 deletions apps/webapp/app/components/runs/v3/TaskTriggerSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { TaskTriggerSource } from "@trigger.dev/database";
import { ClockIcon } from "~/assets/icons/ClockIcon";
import { CubeSparkleIcon } from "~/assets/icons/CubeSparkleIcon";
import { TaskIconSmall } from "~/assets/icons/TaskIcon";
import { WebhookIcon } from "~/assets/icons/WebhookIcon";
import { cn } from "~/utils/cn";

export function TaskTriggerSourceIcon({
Expand All @@ -21,6 +22,11 @@ export function TaskTriggerSourceIcon({
case "AGENT": {
return <CubeSparkleIcon className={cn("size-4.5 min-w-4.5 text-agents", className)} />;
}
case "WEBHOOK": {
return (
<WebhookIcon className={cn("size-[1.125rem] min-w-[1.125rem] text-webhooks", className)} />
);
}
}
}

Expand All @@ -35,5 +41,8 @@ export function taskTriggerSourceDescription(source: TaskTriggerSource) {
case "AGENT": {
return "Agent task";
}
case "WEBHOOK": {
return "Webhook task";
}
}
}
94 changes: 94 additions & 0 deletions apps/webapp/app/components/webhookConsole/ReplaySourcePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { type WebhookDeliveryStatus } from "@trigger.dev/database";
import { useEffect } from "react";
import { useTypedFetcher } from "remix-typedjson";
import { DateTime } from "~/components/primitives/DateTime";
import { Spinner } from "~/components/primitives/Spinner";
import { DeliveryStatusBadge } from "~/components/webhookDeliveries/v1/DeliveryStatus";
import { type loader as replaySourceLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.replay-source";

export function ReplaySourcePicker({
replaySourcePath,
onLoad,
}: {
replaySourcePath: string;
onLoad: (body: string, headers: Record<string, string>) => void;
}) {
const listFetcher = useTypedFetcher<typeof replaySourceLoader>();
const payloadFetcher = useTypedFetcher<typeof replaySourceLoader>();

useEffect(() => {
if (listFetcher.state === "idle" && listFetcher.data === undefined) {
listFetcher.load(replaySourcePath);
}
}, [listFetcher, replaySourcePath]);

useEffect(() => {
const data = payloadFetcher.data;
if (data?.kind === "payload") {
onLoad(data.body, data.headers);
}
}, [payloadFetcher.data, onLoad]);

const listLoading =
listFetcher.state === "loading" ||
(listFetcher.data === undefined && listFetcher.state !== "idle");
const list = listFetcher.data?.kind === "list" ? listFetcher.data.deliveries : [];
const loadingDeliveryId =
payloadFetcher.state !== "idle"
? (new URLSearchParams(payloadFetcher.formAction?.split("?")[1] ?? "").get("deliveryId") ??
undefined)
: undefined;

function selectDelivery(friendlyId: string) {
payloadFetcher.load(`${replaySourcePath}?deliveryId=${encodeURIComponent(friendlyId)}`);
}

return (
<div className="flex h-full flex-col overflow-hidden">
<div className="flex items-center gap-2 border-b border-grid-dimmed px-3 py-2">
<span className="text-xs font-medium text-text-dimmed">
Load a past delivery's payload into the composer
</span>
{listLoading ? <Spinner className="size-3.5" /> : null}
</div>
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
{!listLoading && list.length === 0 ? (
<p className="px-3 py-8 text-center text-sm text-text-dimmed">
No past deliveries to replay yet.
</p>
) : (
list.map((delivery) => (
<button
key={delivery.friendlyId}
type="button"
onClick={() => selectDelivery(delivery.friendlyId)}
disabled={payloadFetcher.state !== "idle"}
className="flex w-full items-center justify-between gap-2 border-b border-grid-dimmed px-3 py-2 text-left hover:bg-charcoal-800 disabled:opacity-60"
>
<span className="flex min-w-0 items-center gap-1.5">
<span className="truncate font-mono text-xs text-text-bright">
{delivery.friendlyId}
</span>
{delivery.isTest ? (
<span className="shrink-0 rounded-sm bg-charcoal-700 px-1 py-0.5 text-xxs font-semibold uppercase tracking-wide text-text-dimmed">
Test
</span>
) : null}
</span>
<span className="flex shrink-0 items-center gap-2">
<span className="text-xxs text-text-dimmed">
<DateTime date={new Date(delivery.createdAt)} />
</span>
{loadingDeliveryId === delivery.friendlyId ? (
<Spinner className="size-3.5" />
) : (
<DeliveryStatusBadge status={delivery.status as WebhookDeliveryStatus} />
)}
</span>
</button>
))
)}
</div>
</div>
);
}
Loading
Loading