Skip to content
Closed
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
1,482 changes: 1,407 additions & 75 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions references/waitpoint-tokens/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Trigger.dev secret key
# https://trigger.dev/docs/apikeys
TRIGGER_SECRET_KEY=
TRIGGER_API_URL=
NEXT_PUBLIC_TRIGGER_API_URL=

# OpenAI API key for generating article summaries.
# https://platform.openai.com
OPENAI_API_KEY=

# ElevenLabs API key for converting text to speech.
# https://elevenlabs.io/docs/quickstart#create-an-api-key
ELEVENLABS_API_KEY=

# Credentials for writing audio streams to an S3-compatible bucket.
# Does not necessarily need to be AWS S3 bucket, as long as it is compatible to the S3 SDK, e.g., https://www.tigrisdata.com/
# https://docs.aws.amazon.com/sdkref/latest/guide/environment-variables.html
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_ENDPOINT_URL_S3=
AWS_ENDPOINT_URL_IAM=
AWS_S3_BUCKET=
AWS_REGION=

# Slack webhook URL for sending messages to a Slack channel.
# Follow the guide in https://api.slack.com/messaging/webhooks to generate a webhook URL.
SLACK_WEBHOOK_URL=
44 changes: 44 additions & 0 deletions references/waitpoint-tokens/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*
!.env.example

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

.trigger
66 changes: 66 additions & 0 deletions references/waitpoint-tokens/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# An AI workflow with a human-in-the-loop approval step

This reference project shows a possible approach to implement workflows using Trigger.dev and [ReactFlow](https://reactflow.dev/).
It makes use of the Trigger.dev Realtime API and the new waitpoint token feature to implement a human-in-the-loop workflow.

## Getting Started

This guide assumes that you have followed the [Contributing.md](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md#setup) instructions to set up a local Trigger.dev instance. If not, please complete the setup before continuing.

1. Run the main Trigger.dev webapp:

```bash
pnpm run dev --filter webapp
```

2. Optionally, build the CLI and SDK if you are working on them and applying changes:

```bash
pnpm run dev --filter trigger.dev --filter "@trigger.dev/*"
```

3. Login with the CLI:

```bash
cd references/trigger-flow
pnpm exec trigger login -a http://localhost:3030
Comment on lines +25 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Directory path inconsistency.

The README instructs users to navigate to "references/trigger-flow", but based on the PR and file structure, the project appears to be in "references/waitpoint-tokens".

-    cd references/trigger-flow
+    cd references/waitpoint-tokens
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cd references/trigger-flow
pnpm exec trigger login -a http://localhost:3030
cd references/waitpoint-tokens
pnpm exec trigger login -a http://localhost:3030

```

Optionally, you can use the `profile` flag to create a new profile:

```bash
pnpm exec trigger login -a http://localhost:3030 --profile local
```

Note that you'll need to use this profile for the subsequent commands.

4. Create an `.env` file by copying [.env.example](.env.example) and fill in the required environment variables. The example file includes a description for each variable.

5. Run the CLI

```bash
pnpm exec trigger dev
```

You should see now the `dev` command spitting out messages, including that it's started a background worker.

6. Run the ReactFlow app:

```bash
pnpm run dev
```

Open [http://localhost:3000](http://localhost:3000) on your browser to checkout the workflow.

## Learn More

To learn more about the technologies used in this project, check out the following resources:

- [Trigger.dev Docs](https://trigger.dev/docs) - learn about Trigger.dev and its features
- [Trigger.dev Waitpoint Token Docs](https://trigger.dev/docs/wait-for-token) - learn about waitpoint tokens in Trigger.dev and human-in-the-loop flows
- [Trigger.dev Realtime Docs](https://trigger.dev/docs/realtime) - learn about the Realtime feature of Trigger.dev
- [Trigger.dev Realtime Streams](https://trigger.dev/docs/realtime/streams) - learn about the different types of streams available in Trigger.dev
- [ReactFlow Docs](https://reactflow.dev/learn) - learn about building interactive diagrams using ReactFlow
- [React Hooks for Trigger.dev](https://trigger.dev/docs/frontend/react-hooks) - learn about the React hooks provided by Trigger.dev
- [ElevenLabs SDK](https://elevenlabs.io/docs/overview) - learn about ElevenLabs' AI audio capabilities
- [AI SDK Documentation](https://sdk.vercel.ai/docs/introduction) - learn about the AI SDK for working with LLMs
7 changes: 7 additions & 0 deletions references/waitpoint-tokens/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
};

export default nextConfig;
41 changes: 41 additions & 0 deletions references/waitpoint-tokens/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "waitpoint-tokens",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"trigger:dev": "trigger dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@ai-sdk/openai": "^1.3.4",
"@aws-sdk/client-s3": "^3.777.0",
"@aws-sdk/s3-request-presigner": "^3.777.0",
"@trigger.dev/react-hooks": "workspace:*",
"@trigger.dev/sdk": "workspace:*",
"@xyflow/react": "^12.4.4",
"ai": "^4.2.8",
"clsx": "^2.1.1",
"elevenlabs": "^1.55.0",
"html-to-text": "^9.0.5",
"lucide-react": "^0.484.0",
"next": "15.2.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-tippy": "^1.4.0",
"tailwind-merge": "^3.2.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add PostCSS as a direct dependency

You're using @tailwindcss/postcss but don't have PostCSS listed as a direct dependency. Consider adding it to ensure proper build compatibility.

  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@trigger.dev/build": "workspace:*",
    "@types/html-to-text": "^9.0.4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
+   "postcss": "^8",
    "tailwindcss": "^4",
    "trigger.dev": "workspace:*",
    "typescript": "^5"
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@tailwindcss/postcss": "^4",
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@trigger.dev/build": "workspace:*",
"@types/html-to-text": "^9.0.4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^4",
"trigger.dev": "workspace:*",
"typescript": "^5"
}

"@trigger.dev/build": "workspace:*",
"@types/html-to-text": "^9.0.4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"trigger.dev": "workspace:*",
"typescript": "^5"
}
}
5 changes: 5 additions & 0 deletions references/waitpoint-tokens/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};

export default config;
78 changes: 78 additions & 0 deletions references/waitpoint-tokens/src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use server";

import type { articleWorkflow } from "@/trigger/articleWorkflow";
import type { ReviewPayload } from "@/trigger/reviewSummary";
import { auth, tasks, wait } from "@trigger.dev/sdk/v3";

const randomStr = (length: number) =>
[...Array(length)]
.map(
() =>
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[
Math.floor(Math.random() * 62)
]
)
.join("");

export async function triggerArticleWorkflow(prevState: any, formData: FormData) {
const articleUrl = formData.get("articleUrl") as string;
const workflowTag = `reactflow_${randomStr(20)}`;

const reviewWaitpointToken = await wait.createToken({
tags: [workflowTag],
timeout: "1h",
idempotencyKey: `review-summary-${workflowTag}`,
});

const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
,
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
Comment on lines +27 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix empty slot in Promise.all array

There's an empty slot in the Promise.all array (line 38) which could cause unexpected behavior. Remove the extra comma.

  const [workflowPublicAccessToken] = await Promise.all([
    // We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
    // This token has access to all runs tagged with the unique workflow tag.
    auth.createPublicToken({
      scopes: {
        read: {
          tags: [workflowTag],
        },
      },
    }),
-    ,
    tasks.trigger<typeof articleWorkflow>(
      "article-workflow",
      {
        articleUrl,
        approvalWaitpointTokenId: reviewWaitpointToken.id,
      },
      {
        tags: [workflowTag],
      }
    ),
  ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
,
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
🧰 Tools
🪛 Biome (1.9.4)

[error] 27-48: This array contains an empty slot.

Unsafe fix: Replace hole with undefined

(lint/suspicious/noSparseArray)


return {
articleUrl,
workflowTag,
workflowPublicAccessToken,
};
}
Comment on lines +17 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for article URL

The function doesn't validate the article URL before triggering the workflow. Consider adding basic validation to ensure it's a valid URL.

export async function triggerArticleWorkflow(prevState: any, formData: FormData) {
  const articleUrl = formData.get("articleUrl") as string;
+  
+  // Validate URL
+  try {
+    new URL(articleUrl);
+  } catch (error) {
+    return {
+      error: "Please enter a valid URL",
+    };
+  }
  
  const workflowTag = `reactflow_${randomStr(20)}`;
  
  // Rest of function remains the same
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function triggerArticleWorkflow(prevState: any, formData: FormData) {
const articleUrl = formData.get("articleUrl") as string;
const workflowTag = `reactflow_${randomStr(20)}`;
const reviewWaitpointToken = await wait.createToken({
tags: [workflowTag],
timeout: "1h",
idempotencyKey: `review-summary-${workflowTag}`,
});
const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
,
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
return {
articleUrl,
workflowTag,
workflowPublicAccessToken,
};
}
export async function triggerArticleWorkflow(prevState: any, formData: FormData) {
const articleUrl = formData.get("articleUrl") as string;
// Validate URL
try {
new URL(articleUrl);
} catch (error) {
return {
error: "Please enter a valid URL",
};
}
const workflowTag = `reactflow_${randomStr(20)}`;
const reviewWaitpointToken = await wait.createToken({
tags: [workflowTag],
timeout: "1h",
idempotencyKey: `review-summary-${workflowTag}`,
});
const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
,
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
return {
articleUrl,
workflowTag,
workflowPublicAccessToken,
};
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 27-48: This array contains an empty slot.

Unsafe fix: Replace hole with undefined

(lint/suspicious/noSparseArray)


export async function approveArticleSummary(tokenId: string) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: true,
approvedAt: new Date(),
approvedBy: "Alice",
}
);
}
Comment on lines +57 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make approver name a parameter instead of hardcoding

Currently, the approver name is hardcoded as "Alice". Consider making it a parameter to allow different users to approve summaries.

-export async function approveArticleSummary(tokenId: string) {
+export async function approveArticleSummary(tokenId: string, approverName: string = "Alice") {
  await wait.completeToken<ReviewPayload>(
    { id: tokenId },
    {
      approved: true,
      approvedAt: new Date(),
-      approvedBy: "Alice",
+      approvedBy: approverName,
    }
  );
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function approveArticleSummary(tokenId: string) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: true,
approvedAt: new Date(),
approvedBy: "Alice",
}
);
}
export async function approveArticleSummary(tokenId: string, approverName: string = "Alice") {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: true,
approvedAt: new Date(),
approvedBy: approverName,
}
);
}


export async function rejectArticleSummary(tokenId: string) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: false,
rejectedAt: new Date(),
rejectedBy: "Alice",
reason: "It's no good",
}
);
}
Comment on lines +68 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make rejection parameters configurable

Similar to the approve function, the rejection function has hardcoded values for the rejector name and reason. Make these configurable parameters.

-export async function rejectArticleSummary(tokenId: string) {
+export async function rejectArticleSummary(
+  tokenId: string, 
+  rejectorName: string = "Alice", 
+  reason: string = "It's no good"
+) {
  await wait.completeToken<ReviewPayload>(
    { id: tokenId },
    {
      approved: false,
      rejectedAt: new Date(),
-      rejectedBy: "Alice",
-      reason: "It's no good",
+      rejectedBy: rejectorName,
+      reason: reason,
    }
  );
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function rejectArticleSummary(tokenId: string) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: false,
rejectedAt: new Date(),
rejectedBy: "Alice",
reason: "It's no good",
}
);
}
export async function rejectArticleSummary(
tokenId: string,
rejectorName: string = "Alice",
reason: string = "It's no good"
) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: false,
rejectedAt: new Date(),
rejectedBy: rejectorName,
reason: reason,
}
);
}

Binary file added references/waitpoint-tokens/src/app/favicon.ico
Binary file not shown.
29 changes: 29 additions & 0 deletions references/waitpoint-tokens/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@import "tailwindcss";
@import "react-tippy/dist/tippy.css";



:root {
--background: #ffffff;
--foreground: #171717;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
Comment on lines +14 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Font variables reference undefined fonts.

The CSS variables --font-geist-sans and --font-geist-mono are referenced but not defined anywhere in the file. Either import the Geist font family or define these variables properly.

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
-  --font-sans: var(--font-geist-sans);
-  --font-mono: var(--font-geist-mono);
+  --font-sans: Arial, Helvetica, sans-serif;
+  --font-mono: monospace;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: Arial, Helvetica, sans-serif;
--font-mono: monospace;
}

}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
30 changes: 30 additions & 0 deletions references/waitpoint-tokens/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Trigger Flow",
description: "Trigger.dev reference project with ReactFlow",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
</html>
);
}
Loading
Loading