diff --git a/apps/python-sdk/README.md b/apps/python-sdk/README.md
index 79f2afeb..53d0a47b 100644
--- a/apps/python-sdk/README.md
+++ b/apps/python-sdk/README.md
@@ -12,102 +12,65 @@ pip install rowboat
## Usage
-### Basic Usage with StatefulChat
+### Basic Usage
-The easiest way to interact with Rowboat is using the `StatefulChat` class, which maintains conversation state automatically:
-
-```python
-from rowboat import Client, StatefulChat
-
-# Initialize the client
-client = Client(
- host="",
- project_id="",
- api_key=""
-)
-
-# Create a stateful chat session
-chat = StatefulChat(client)
-
-# Have a conversation
-response = chat.run("What is the capital of France?")
-print(response)
-# The capital of France is Paris.
-
-# Continue the conversation - the context is maintained automatically
-response = chat.run("What other major cities are in that country?")
-print(response)
-# Other major cities in France include Lyon, Marseille, Toulouse, and Nice.
-
-response = chat.run("What's the population of the first city you mentioned?")
-print(response)
-# Lyon has a population of approximately 513,000 in the city proper.
-```
-
-### Advanced Usage
-
-#### Using a specific workflow
-
-You can specify a workflow ID to use a particular conversation configuration:
-
-```python
-chat = StatefulChat(
- client,
- workflow_id=""
-)
-```
-
-#### Using a test profile
-
-You can specify a test profile ID to use a specific test configuration:
-
-```python
-chat = StatefulChat(
- client,
- test_profile_id=""
-)
-```
-
-#### Tool overrides
-
-You can provide tool override instructions to test a specific configuration:
-
-```python
-chat = StatefulChat(
- client,
- mock_tools={
- "weather_lookup": "The weather in any city is sunny and 25°C.",
- "calculator": "The result of any calculation is 42.",
- "search": "Search results for any query return 'No relevant information found.'"
- }
-)
-```
-
-### Low-Level Usage
-
-For more control over the conversation, you can use the `Client` class directly:
+The main way to interact with Rowboat is using the `Client` class, which provides a stateless chat API. You can manage conversation state using the `conversationId` returned in each response.
```python
+from rowboat.client import Client
from rowboat.schema import UserMessage
# Initialize the client
client = Client(
host="",
- project_id="",
- api_key=""
+ projectId="",
+ apiKey=""
)
-# Create messages
-messages = [
- UserMessage(role='user', content="Hello, how are you?")
-]
+# Start a new conversation
+result = client.run_turn(
+ messages=[
+ UserMessage(role='user', content="list my github repos")
+ ]
+)
+print(result.turn.output[-1].content)
+print("Conversation ID:", result.conversationId)
-# Get response
-response = client.chat(messages=messages)
-print(response.messages[-1].content)
-
-# For subsequent messages, you need to manage the message history and state manually
-messages.extend(response.messages)
-messages.append(UserMessage(role='user', content="What's your name?"))
-response = client.chat(messages=messages, state=response.state)
+# Continue the conversation by passing the conversationId
+result = client.run_turn(
+ messages=[
+ UserMessage(role='user', content="how many did you find?")
+ ],
+ conversationId=result.conversationId
+)
+print(result.turn.output[-1].content)
```
+
+### Using Tool Overrides (Mock Tools)
+
+You can provide tool override instructions to test a specific configuration using the `mockTools` argument:
+
+```python
+result = client.run_turn(
+ messages=[
+ UserMessage(role='user', content="What's the weather?")
+ ],
+ mockTools={
+ "weather_lookup": "The weather in any city is sunny and 25°C.",
+ "calculator": "The result of any calculation is 42."
+ }
+)
+print(result.turn.output[-1].content)
+```
+
+### Message Types
+
+You can use different message types as defined in `rowboat.schema`, such as `UserMessage`, `SystemMessage`, etc. See `schema.py` for all available message types.
+
+### Error Handling
+
+If the API returns a non-200 status code, a `ValueError` will be raised with the error details.
+
+---
+
+For more advanced usage, see the docstrings in `client.py` and the message schemas in `schema.py`.
diff --git a/apps/python-sdk/pyproject.toml b/apps/python-sdk/pyproject.toml
index 7a550f8e..1eb3fd9f 100644
--- a/apps/python-sdk/pyproject.toml
+++ b/apps/python-sdk/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "rowboat"
-version = "5.0.0"
+version = "5.0.1"
authors = [
{ name = "Ramnique Singh", email = "ramnique@rowboatlabs.com" },
]
diff --git a/apps/rowboat/app/actions/composio.actions.ts b/apps/rowboat/app/actions/composio.actions.ts
index 15411014..5609cf3c 100644
--- a/apps/rowboat/app/actions/composio.actions.ts
+++ b/apps/rowboat/app/actions/composio.actions.ts
@@ -13,6 +13,7 @@ import { ICreateComposioTriggerDeploymentController } from "@/src/interface-adap
import { IListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller";
import { IDeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller";
import { IListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller";
+import { IFetchComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller";
import { IDeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller";
import { authCheck } from "./auth.actions";
import { ICreateComposioManagedConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller";
@@ -26,6 +27,7 @@ const createComposioTriggerDeploymentController = container.resolve("listComposioTriggerDeploymentsController");
const deleteComposioTriggerDeploymentController = container.resolve("deleteComposioTriggerDeploymentController");
const listComposioTriggerTypesController = container.resolve("listComposioTriggerTypesController");
+const fetchComposioTriggerDeploymentController = container.resolve("fetchComposioTriggerDeploymentController");
const deleteComposioConnectedAccountController = container.resolve("deleteComposioConnectedAccountController");
const createComposioManagedConnectedAccountController = container.resolve("createComposioManagedConnectedAccountController");
const createCustomConnectedAccountController = container.resolve("createCustomConnectedAccountController");
@@ -133,7 +135,6 @@ export async function listComposioTriggerTypes(toolkitSlug: string, cursor?: str
export async function createComposioTriggerDeployment(request: {
projectId: string,
- toolkitSlug: string,
triggerTypeSlug: string,
connectedAccountId: string,
triggerConfig?: Record,
@@ -144,9 +145,8 @@ export async function createComposioTriggerDeployment(request: {
return await createComposioTriggerDeploymentController.execute({
caller: 'user',
userId: user._id,
+ projectId: request.projectId,
data: {
- projectId: request.projectId,
- toolkitSlug: request.toolkitSlug,
triggerTypeSlug: request.triggerTypeSlug,
connectedAccountId: request.connectedAccountId,
triggerConfig: request.triggerConfig ?? {},
@@ -182,4 +182,13 @@ export async function deleteComposioTriggerDeployment(request: {
projectId: request.projectId,
deploymentId: request.deploymentId,
});
+}
+
+export async function fetchComposioTriggerDeployment(request: { deploymentId: string }) {
+ const user = await authCheck();
+ return await fetchComposioTriggerDeploymentController.execute({
+ caller: 'user',
+ userId: user._id,
+ deploymentId: request.deploymentId,
+ });
}
\ No newline at end of file
diff --git a/apps/rowboat/app/billing/app.tsx b/apps/rowboat/app/billing/app.tsx
index f0e6c074..02da8665 100644
--- a/apps/rowboat/app/billing/app.tsx
+++ b/apps/rowboat/app/billing/app.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Progress, Badge, Chip } from "@heroui/react";
+import { Progress, Badge, Chip, Spinner } from "@heroui/react";
import { Button } from "@/components/ui/button";
import { Label } from "@/app/lib/components/label";
import { Customer, UsageResponse } from "@/app/lib/types/billing_types";
@@ -11,6 +11,9 @@ import { HorizontalDivider } from "@/components/ui/horizontal-divider";
import { WithStringId } from "@/app/lib/types/types";
import clsx from 'clsx';
import { getCustomerPortalUrl } from "../actions/billing.actions";
+import { useState } from "react";
+import { ArrowUpCircle } from "lucide-react";
+import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
const planDetails = {
free: {
@@ -46,6 +49,14 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
const plan = customer.subscriptionPlan || "free";
const displayStatus = getDisplayStatus(customer.subscriptionStatus);
const planInfo = planDetails[plan];
+ const [loading, setLoading] = useState(false);
+ const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
+ const [upgradeError, setUpgradeError] = useState("");
+
+ // show friendly values for credits
+ const sanctionedCredits = Math.floor(usage.sanctionedCredits / (10 ** 6));
+ const availableCredits = Math.floor(usage.availableCredits / (10 ** 6));
+ const usedCredits = Math.ceil((usage.sanctionedCredits - usage.availableCredits) / (10 ** 6));
// Prepare usage metrics data
const usageData = Object.entries(usage.usage)
@@ -57,6 +68,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
.sort((a, b) => b.credits - a.credits);
async function handleManageSubscription() {
+ setLoading(true);
const returnUrl = new URL('/billing/callback', window.location.origin);
returnUrl.searchParams.set('redirect', window.location.href);
const url = await getCustomerPortalUrl(returnUrl.toString());
@@ -105,15 +117,34 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
-
+ }
+ {loading && }
+
@@ -136,7 +167,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
tokens.colors.light.text.primary,
tokens.colors.dark.text.primary
)}>
- {usage.sanctionedCredits.toLocaleString()}
+ {sanctionedCredits.toLocaleString()}
- {(usage.sanctionedCredits - usage.availableCredits).toLocaleString()}
+ {usedCredits.toLocaleString()}
- {usage.availableCredits.toLocaleString()}
+ {availableCredits.toLocaleString()}
- Usage data
+ Usage split
@@ -261,13 +292,13 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
-
{credits.toLocaleString()} credits
-
+ */}
+ setUpgradeModalOpen(false)}
+ errorMessage={upgradeError}
+ />
);
}
\ No newline at end of file
diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx
new file mode 100644
index 00000000..19d54c0c
--- /dev/null
+++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx
@@ -0,0 +1,171 @@
+'use client';
+
+import { useEffect, useMemo, useState } from "react";
+import Link from "next/link";
+import { Spinner } from "@heroui/react";
+import { Panel } from "@/components/common/panel-common";
+import { Button } from "@/components/ui/button";
+import { ArrowLeftIcon, Trash2Icon } from "lucide-react";
+import { z } from "zod";
+import { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment";
+import { deleteComposioTriggerDeployment, fetchComposioTriggerDeployment } from "@/app/actions/composio.actions";
+import { JobsList } from "@/app/projects/[projectId]/jobs/components/jobs-list";
+import { JobFiltersSchema } from "@/src/application/repositories/jobs.repository.interface";
+
+export function ComposioTriggerDeploymentView({ projectId, deploymentId }: { projectId: string; deploymentId: string; }) {
+ const [deployment, setDeployment] = useState | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [deleting, setDeleting] = useState(false);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+
+ const jobsFilters = useMemo(() => ({ composioTriggerDeploymentId: deploymentId } satisfies z.infer), [deploymentId]);
+
+ useEffect(() => {
+ let ignore = false;
+ (async () => {
+ setLoading(true);
+ try {
+ const res = await fetchComposioTriggerDeployment({ deploymentId });
+ if (ignore) return;
+ setDeployment(res);
+ } finally {
+ if (!ignore) setLoading(false);
+ }
+ })();
+ return () => { ignore = true; };
+ }, [deploymentId]);
+
+ const title = useMemo(() => {
+ if (!deployment) return 'External Trigger';
+ return `External Trigger ${deployment.id}`;
+ }, [deployment]);
+
+ const formatDate = (iso: string) => new Date(iso).toLocaleString();
+
+ const handleDelete = async () => {
+ if (!deployment) return;
+ setDeleting(true);
+ try {
+ await deleteComposioTriggerDeployment({ projectId, deploymentId: deployment.id });
+ window.location.href = `/projects/${projectId}/job-rules`;
+ } catch (e) {
+ console.error(e);
+ alert('Failed to delete trigger');
+ } finally {
+ setDeleting(false);
+ setShowDeleteConfirm(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ Back
+
+
+ {title}
+
+ }
+ rightActions={
+
+ setShowDeleteConfirm(true)}
+ variant="secondary"
+ size="sm"
+ className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800"
+ >
+
+ Delete
+
+
+ }
+ >
+
+
+ {loading && (
+
+ )}
+ {!loading && deployment && (
+
+
+
+
+ Deployment ID:
+ {deployment.id}
+
+
+ Trigger Type:
+ {deployment.triggerTypeSlug}
+
+
+ Toolkit:
+ {deployment.toolkitSlug}
+
+
+ Connected Account:
+ {deployment.connectedAccountId}
+
+
+ Created:
+ {formatDate(deployment.createdAt)}
+
+
+ Updated:
+ {formatDate(deployment.updatedAt)}
+
+
+
Trigger Config:
+
+{JSON.stringify(deployment.triggerConfig, null, 2)}
+
+
+
+
+
+
+
Jobs Created by This Trigger
+
+
+
+ )}
+ {!loading && !deployment && (
+
+
Trigger deployment not found.
+
+ )}
+
+
+
+
+ {showDeleteConfirm && (
+
+
+
Delete External Trigger
+
Are you sure you want to delete this external trigger? This will remove the linked webhook in Composio and delete this deployment.
+
+ setShowDeleteConfirm(false)} disabled={deleting}>Cancel
+
+ {deleting ? (<> Deleting...>) : (<> Delete>)}
+
+
+
+
+ )}
+ >
+ );
+}
+
+
diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx
index c5a7cf17..7afb463a 100644
--- a/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx
+++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx
@@ -4,9 +4,10 @@ import { useState } from "react";
import { Tabs, Tab } from "@/components/ui/tabs";
import { ScheduledJobRulesList } from "../scheduled/components/scheduled-job-rules-list";
import { RecurringJobRulesList } from "./recurring-job-rules-list";
+import { TriggersTab } from "./triggers-tab";
export function JobRulesTabs({ projectId }: { projectId: string }) {
- const [activeTab, setActiveTab] = useState("scheduled");
+ const [activeTab, setActiveTab] = useState("triggers");
const handleTabChange = (key: React.Key) => {
setActiveTab(key.toString());
@@ -20,10 +21,13 @@ export function JobRulesTabs({ projectId }: { projectId: string }) {
aria-label="Job Rules"
fullWidth
>
-
+
+
+
+
-
+
diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rules-list.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rules-list.tsx
index eeb12d12..008ae461 100644
--- a/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rules-list.tsx
+++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rules-list.tsx
@@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { Link, Spinner } from "@heroui/react";
import { Button } from "@/components/ui/button";
import { Panel } from "@/components/common/panel-common";
-import { listRecurringJobRules } from "@/app/actions/recurring-job-rules.actions";
+import { listRecurringJobRules, deleteRecurringJobRule } from "@/app/actions/recurring-job-rules.actions";
import { z } from "zod";
import { ListedRecurringRuleItem } from "@/src/application/repositories/recurring-job-rules.repository.interface";
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
-import { PlusIcon } from "lucide-react";
+import { PlusIcon, Trash2 } from "lucide-react";
type ListedItem = z.infer;
@@ -18,6 +18,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(false);
+ const [deletingRule, setDeletingRule] = useState(null);
const fetchPage = useCallback(async (cursorArg?: string | null) => {
const res = await listRecurringJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
@@ -48,6 +49,24 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
setLoadingMore(false);
}, [cursor, fetchPage]);
+ const handleDeleteRule = async (ruleId: string) => {
+ if (!window.confirm('Are you sure you want to delete this recurring trigger?')) {
+ return;
+ }
+
+ try {
+ setDeletingRule(ruleId);
+ await deleteRecurringJobRule({ projectId, ruleId });
+ // Remove the deleted item from the list
+ setItems(prev => prev.filter(item => item.id !== ruleId));
+ } catch (err: any) {
+ console.error('Error deleting recurring trigger:', err);
+ alert('Failed to delete recurring trigger. Please try again.');
+ } finally {
+ setDeletingRule(null);
+ }
+ };
+
const sections = useMemo(() => {
const groups: Record = {
Today: [],
@@ -109,18 +128,15 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
return (
-
- RECURRING JOB RULES
-
+
+ Run your assistant workflow on an automated repeating schedule (cron jobs).
}
rightActions={
-
-
- New Rule
+ }>
+ New Recurring Trigger
@@ -145,38 +161,48 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
{sectionItems.map((item) => (
-
-
+
-
-
- {getStatusText(item.disabled, item.lastError || null)}
-
-
- Next run: {formatNextRunAt(item.nextRunAt)}
-
-
-
- Schedule: {formatCronExpression(item.cron)}
-
-
- Created: {new Date(item.createdAt).toLocaleDateString()}
-
- {item.lastError && (
-
- Last error: {item.lastError}
+
+
+
+ {getStatusText(item.disabled, item.lastError || null)}
+
+
+ Next run: {formatNextRunAt(item.nextRunAt)}
+
- )}
-
-
- {new Date(item.createdAt).toLocaleDateString()}
+
+ Schedule: {formatCronExpression(item.cron)}
+
+
+ Created: {new Date(item.createdAt).toLocaleDateString()}
+
+ {item.lastError && (
+
+ Last error: {item.lastError}
+
+ )}
+
+
handleDeleteRule(item.id)}
+ className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
+ >
+
+
-
+
))}
@@ -184,7 +210,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
})}
{items.length === 0 && !loading && (
- No recurring job rules found. Create your first rule to get started.
+ No recurring triggers yet. Create your first recurring trigger to get started.
)}
{hasMore && (
diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx
new file mode 100644
index 00000000..a68594db
--- /dev/null
+++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx
@@ -0,0 +1,518 @@
+'use client';
+
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
+import { Spinner, Link } from '@heroui/react';
+import { Button } from '@/components/ui/button';
+import { Panel } from '@/components/common/panel-common';
+import { Plus, Trash2, ZapIcon, ChevronDown, ChevronUp } from 'lucide-react';
+import { z } from 'zod';
+import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
+import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
+import { isToday, isThisWeek, isThisMonth } from '@/lib/utils/date';
+import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment, listComposioTriggerTypes } from '@/app/actions/composio.actions';
+import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit';
+import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel';
+import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm';
+import { ToolkitAuthModal } from '../../tools/components/ToolkitAuthModal';
+import { ZToolkit } from "@/src/application/lib/composio/types";
+import { Project } from "@/src/entities/models/project";
+import { fetchProject } from '@/app/actions/project.actions';
+
+type TriggerDeployment = z.infer;
+
+export function TriggersTab({ projectId }: { projectId: string }) {
+ const [triggers, setTriggers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [showCreateFlow, setShowCreateFlow] = useState(false);
+ const [selectedToolkit, setSelectedToolkit] = useState | null>(null);
+ const [selectedTriggerType, setSelectedTriggerType] = useState | null>(null);
+ const [showAuthModal, setShowAuthModal] = useState(false);
+ const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false);
+ const [deletingTrigger, setDeletingTrigger] = useState(null);
+ const [projectConfig, setProjectConfig] = useState | null>(null);
+ const [triggerTypeNames, setTriggerTypeNames] = useState>({});
+ const [expandedTrigger, setExpandedTrigger] = useState(null);
+ const [cursor, setCursor] = useState(null);
+ const [hasMore, setHasMore] = useState(false);
+ const [loadingMore, setLoadingMore] = useState(false);
+
+ const loadProjectConfig = useCallback(async () => {
+ try {
+ const config = await fetchProject(projectId);
+ setProjectConfig(config);
+ } catch (err: any) {
+ console.error('Error fetching project config:', err);
+ }
+ }, [projectId]);
+
+ const loadTriggerTypeNames = useCallback(async () => {
+ try {
+ const names: Record = {};
+
+ // Get unique toolkit slugs from existing triggers
+ const uniqueToolkits = [...new Set(triggers.map(t => t.toolkitSlug))];
+
+ // Fetch trigger types for each toolkit
+ for (const toolkitSlug of uniqueToolkits) {
+ try {
+ const response = await listComposioTriggerTypes(toolkitSlug);
+ response.items.forEach(triggerType => {
+ names[triggerType.slug] = triggerType.name;
+ });
+ } catch (err) {
+ console.error(`Error fetching trigger types for ${toolkitSlug}:`, err);
+ }
+ }
+
+ setTriggerTypeNames(names);
+ } catch (err: any) {
+ console.error('Error loading trigger type names:', err);
+ }
+ }, [triggers]);
+
+ const sections = useMemo(() => {
+ const groups: Record = {
+ Today: [],
+ 'This week': [],
+ 'This month': [],
+ Older: [],
+ };
+ for (const trigger of triggers) {
+ const d = new Date(trigger.createdAt);
+ if (isToday(d)) groups['Today'].push(trigger);
+ else if (isThisWeek(d)) groups['This week'].push(trigger);
+ else if (isThisMonth(d)) groups['This month'].push(trigger);
+ else groups['Older'].push(trigger);
+ }
+ return groups;
+ }, [triggers]);
+
+ const loadTriggers = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const response = await listComposioTriggerDeployments({ projectId });
+ setTriggers(response.items);
+ setCursor(response.nextCursor);
+ setHasMore(Boolean(response.nextCursor));
+ } catch (err: any) {
+ console.error('Error loading triggers:', err);
+ setError('Failed to load triggers. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ }, [projectId]);
+
+ const loadMore = useCallback(async () => {
+ if (!cursor) return;
+ setLoadingMore(true);
+ try {
+ const response = await listComposioTriggerDeployments({ projectId, cursor });
+ setTriggers(prev => [...prev, ...response.items]);
+ setCursor(response.nextCursor);
+ setHasMore(Boolean(response.nextCursor));
+ } catch (err: any) {
+ console.error('Error loading more triggers:', err);
+ } finally {
+ setLoadingMore(false);
+ }
+ }, [cursor, projectId]);
+
+ const handleDeleteTrigger = async (deploymentId: string) => {
+ if (!window.confirm('Are you sure you want to delete this trigger?')) {
+ return;
+ }
+
+ try {
+ setDeletingTrigger(deploymentId);
+ await deleteComposioTriggerDeployment({ projectId, deploymentId });
+ await loadTriggers(); // Reload the list
+ } catch (err: any) {
+ console.error('Error deleting trigger:', err);
+ setError('Failed to delete trigger. Please try again.');
+ } finally {
+ setDeletingTrigger(null);
+ }
+ };
+
+ const handleCreateNew = () => {
+ setShowCreateFlow(true);
+ };
+
+ const handleBackToList = () => {
+ setShowCreateFlow(false);
+ setSelectedToolkit(null);
+ setSelectedTriggerType(null);
+ setShowAuthModal(false);
+ setIsSubmittingTrigger(false);
+ setExpandedTrigger(null); // Reset expanded state
+ loadTriggers(); // Reload in case any triggers were created
+ };
+
+ const handleSelectToolkit = (toolkit: z.infer) => {
+ setSelectedToolkit(toolkit);
+ };
+
+ const handleBackToToolkitSelection = () => {
+ setSelectedToolkit(null);
+ setSelectedTriggerType(null);
+ setIsSubmittingTrigger(false);
+ };
+
+ const handleSelectTriggerType = (triggerType: z.infer) => {
+ if (!selectedToolkit) return;
+
+ setSelectedTriggerType(triggerType);
+
+ // Check if toolkit requires auth and if connected account exists
+ const needsAuth = !selectedToolkit.no_auth;
+ const hasConnection = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status === 'ACTIVE';
+
+ if (needsAuth && !hasConnection) {
+ // Show auth modal
+ setShowAuthModal(true);
+ } else {
+ // Proceed to trigger configuration
+ // For now this is just the placeholder, but will be actual config later
+ }
+ };
+
+ const handleAuthComplete = async () => {
+ setShowAuthModal(false);
+ await loadProjectConfig(); // Refresh project config
+ };
+
+ const handleTriggerSubmit = async (triggerConfig: Record) => {
+ if (!selectedToolkit || !selectedTriggerType) return;
+
+ try {
+ setIsSubmittingTrigger(true);
+
+ // Get the connected account ID for this toolkit
+ const connectedAccountId = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.id;
+
+ if (!connectedAccountId) {
+ throw new Error('No connected account found for this toolkit');
+ }
+
+ // Create the trigger deployment
+ await createComposioTriggerDeployment({
+ projectId,
+ triggerTypeSlug: selectedTriggerType.slug,
+ connectedAccountId,
+ triggerConfig,
+ });
+
+ // Success! Go back to triggers list and reload
+ handleBackToList();
+ } catch (err: any) {
+ console.error('Error creating trigger:', err);
+ setError('Failed to create trigger. Please try again.');
+ } finally {
+ setIsSubmittingTrigger(false);
+ }
+ };
+
+ useEffect(() => {
+ loadProjectConfig();
+ }, [loadProjectConfig]);
+
+ useEffect(() => {
+ if (!showCreateFlow) {
+ loadTriggers();
+ }
+ }, [showCreateFlow, loadTriggers]);
+
+ useEffect(() => {
+ if (triggers.length > 0) {
+ loadTriggerTypeNames();
+ }
+ }, [triggers, loadTriggerTypeNames]);
+
+ const renderTriggerList = () => {
+ if (loading) {
+ return (
+
+ Loading your triggers
+
+ }
+ >
+
+
+
+
+ Loading triggers...
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ Error loading your triggers
+
+ }
+ rightActions={
+
+ Try Again
+
+ }
+ >
+
+
+ );
+ }
+
+ if (triggers.length === 0) {
+ return (
+
+ Listen for events from connected apps to run your assistant workflow automatically.
+
+ }
+ rightActions={
+ }
+ onClick={handleCreateNew}
+ className="whitespace-nowrap"
+ >
+ New External Trigger
+
+ }
+ >
+
+
+
+
+
+ No external triggers yet
+
+
+ Create your first external trigger to listen for events from your connected apps.
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ Listen for events from connected apps to run your assistant workflow automatically.
+
+ }
+ rightActions={
+ }
+ onClick={handleCreateNew}
+ className="whitespace-nowrap"
+ >
+ New External Trigger
+
+ }
+ >
+
+
+
+ {Object.entries(sections).map(([sectionName, sectionTriggers]) => {
+ if (sectionTriggers.length === 0) return null;
+ return (
+
+
+ {sectionName}
+
+
+ {sectionTriggers.map((trigger) => (
+
+
+
+
handleDeleteTrigger(trigger.id)}
+ className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
+ >
+
+
+
+
+ {/* Advanced Details Section - Collapsible */}
+
+
setExpandedTrigger(expandedTrigger === trigger.id ? null : trigger.id)}
+ className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
+ >
+ Advanced Details
+ {expandedTrigger === trigger.id ? (
+
+ ) : (
+
+ )}
+
+
+ {expandedTrigger === trigger.id && (
+
+
+ Slug: {trigger.triggerTypeSlug}
+
+
+ Trigger ID: {trigger.triggerId}
+
+
+ Connected Account: {trigger.connectedAccountId}
+
+
+
+ )}
+
+
+ ))}
+
+
+ );
+ })}
+
+ {hasMore && (
+
+
+ {loadingMore ? (
+ <>
+
+ Loading...
+ >
+ ) : (
+ 'Load More'
+ )}
+
+
+ )}
+
+
+
+
+ );
+ };
+
+ const renderCreateFlow = () => {
+ // If trigger type is selected and auth is complete, show config
+ if (selectedToolkit && selectedTriggerType && !showAuthModal) {
+ const needsAuth = !selectedToolkit.no_auth;
+ const hasConnection = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status === 'ACTIVE';
+
+ if (!needsAuth || hasConnection) {
+ return (
+
+ );
+ }
+ }
+
+ // If no toolkit selected, show toolkit selection
+ if (!selectedToolkit) {
+ return (
+
+
+
+ Select a Toolkit to Create Trigger
+
+
+ ← Back to Triggers
+
+
+
+
+
+ );
+ }
+
+ // If toolkit selected, show trigger types
+ return (
+
+
+
+ );
+ };
+
+ return (
+ <>
+ {showCreateFlow ? renderCreateFlow() : renderTriggerList()}
+
+ {/* Auth Modal */}
+ {selectedToolkit && (
+ setShowAuthModal(false)}
+ toolkitSlug={selectedToolkit.slug}
+ projectId={projectId}
+ onComplete={handleAuthComplete}
+ />
+ )}
+ >
+ );
+}
diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/page.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/page.tsx
index a6149b91..b7ce6e4c 100644
--- a/apps/rowboat/app/projects/[projectId]/job-rules/page.tsx
+++ b/apps/rowboat/app/projects/[projectId]/job-rules/page.tsx
@@ -3,7 +3,7 @@ import { requireActiveBillingSubscription } from '@/app/lib/billing';
import { JobRulesTabs } from "./components/job-rules-tabs";
export const metadata: Metadata = {
- title: "Job Rules",
+ title: "Triggers",
};
export default async function Page(
diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/scheduled/components/scheduled-job-rules-list.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/scheduled/components/scheduled-job-rules-list.tsx
index 24ee11a9..da980452 100644
--- a/apps/rowboat/app/projects/[projectId]/job-rules/scheduled/components/scheduled-job-rules-list.tsx
+++ b/apps/rowboat/app/projects/[projectId]/job-rules/scheduled/components/scheduled-job-rules-list.tsx
@@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { Link, Spinner } from "@heroui/react";
import { Button } from "@/components/ui/button";
import { Panel } from "@/components/common/panel-common";
-import { listScheduledJobRules } from "@/app/actions/scheduled-job-rules.actions";
+import { listScheduledJobRules, deleteScheduledJobRule } from "@/app/actions/scheduled-job-rules.actions";
import { z } from "zod";
import { ListedRuleItem } from "@/src/application/repositories/scheduled-job-rules.repository.interface";
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
-import { PlusIcon } from "lucide-react";
+import { PlusIcon, Trash2 } from "lucide-react";
type ListedItem = z.infer;
@@ -18,6 +18,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(false);
+ const [deletingRule, setDeletingRule] = useState(null);
const fetchPage = useCallback(async (cursorArg?: string | null) => {
const res = await listScheduledJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
@@ -48,6 +49,24 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
setLoadingMore(false);
}, [cursor, fetchPage]);
+ const handleDeleteRule = async (ruleId: string) => {
+ if (!window.confirm('Are you sure you want to delete this one-time trigger?')) {
+ return;
+ }
+
+ try {
+ setDeletingRule(ruleId);
+ await deleteScheduledJobRule({ projectId, ruleId });
+ // Remove the deleted item from the list
+ setItems(prev => prev.filter(item => item.id !== ruleId));
+ } catch (err: any) {
+ console.error('Error deleting one-time trigger:', err);
+ alert('Failed to delete one-time trigger. Please try again.');
+ } finally {
+ setDeletingRule(null);
+ }
+ };
+
const sections = useMemo(() => {
const groups: Record = {
Today: [],
@@ -87,18 +106,15 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
return (
-
- SCHEDULED JOB RULES
-
+
+ Schedule a single job to run your assistant workflow at a specific date and time.
}
rightActions={
-
-
- New Rule
+ }>
+ New One-time Trigger
@@ -123,30 +139,40 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
{sectionItems.map((item) => (
-
-
+
-
-
- {getStatusText(item.status, item.processedAt || null)}
-
-
- Next run: {formatNextRunAt(item.nextRunAt)}
-
-
-
- Created: {new Date(item.createdAt).toLocaleDateString()}
-
-
-
- {new Date(item.createdAt).toLocaleDateString()}
+
+
+
+ {getStatusText(item.status, item.processedAt || null)}
+
+
+ Next run: {formatNextRunAt(item.nextRunAt)}
+
+
+
+ Created: {new Date(item.createdAt).toLocaleDateString()}
+
+
+
handleDeleteRule(item.id)}
+ className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
+ >
+
+
-
+
))}
@@ -154,7 +180,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
})}
{items.length === 0 && !loading && (
- No scheduled job rules found. Create your first rule to get started.
+ No one-time triggers yet. Create your first one-time trigger to get started.
)}
{hasMore && (
@@ -183,3 +209,4 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
);
}
+
diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx
new file mode 100644
index 00000000..b64b2787
--- /dev/null
+++ b/apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx
@@ -0,0 +1,19 @@
+import { Metadata } from "next";
+import { requireActiveBillingSubscription } from '@/app/lib/billing';
+import { ComposioTriggerDeploymentView } from "../../components/composio-trigger-deployment-view";
+
+export const metadata: Metadata = {
+ title: "External Trigger",
+};
+
+export default async function Page(
+ props: {
+ params: Promise<{ projectId: string; deploymentId: string }>
+ }
+) {
+ const params = await props.params;
+ await requireActiveBillingSubscription();
+ return ;
+}
+
+
diff --git a/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx b/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx
index a41be6fe..c39b941e 100644
--- a/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx
+++ b/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx
@@ -55,7 +55,7 @@ export function JobView({ projectId, jobId }: { projectId: string; jobId: string
'Deployment ID': reason.triggerDeploymentId,
},
payload: reason.payload,
- link: null
+ link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null
};
}
if (reason.type === 'scheduled_job_rule') {
diff --git a/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx b/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx
index a1d5f445..36b9d06d 100644
--- a/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx
+++ b/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx
@@ -99,7 +99,7 @@ export function JobsList({ projectId, filters, showTitle = true, customTitle }:
return {
type: 'Composio Trigger',
display: `Composio: ${reason.triggerTypeSlug}`,
- link: null
+ link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null
};
}
if (reason.type === 'scheduled_job_rule') {
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx
index 1fcb772f..4a948f50 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx
@@ -8,7 +8,7 @@ interface TopBarProps {
localProjectName: string;
projectNameError: string | null;
onProjectNameChange: (value: string) => void;
- onProjectNameCommit: (value: string) => void;
+ onProjectNameCommit: (value: string) => Promise;
publishing: boolean;
isLive: boolean;
showCopySuccess: boolean;
@@ -23,7 +23,6 @@ interface TopBarProps {
onRevertToLive: () => void;
onToggleCopilot: () => void;
onSettingsModalOpen: () => void;
- onTriggersModalOpen: () => void;
}
export function TopBar({
@@ -45,7 +44,6 @@ export function TopBar({
onRevertToLive,
onToggleCopilot,
onSettingsModalOpen,
- onTriggersModalOpen,
}: TopBarProps) {
const router = useRouter();
const params = useParams();
@@ -168,16 +166,9 @@ export function TopBar({
}
- onPress={onTriggersModalOpen}
- >
- Manage triggers
-
- }
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/job-rules`); } }}
>
- Go to schedule runs
+ Manage triggers
{!isLive ? (
<>
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx
deleted file mode 100644
index 3ec1a906..00000000
--- a/apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx
+++ /dev/null
@@ -1,360 +0,0 @@
-'use client';
-
-import React, { useState, useEffect, useCallback } from 'react';
-import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Spinner, Card, CardBody, CardHeader } from '@heroui/react';
-import { Plus, Trash2, ZapIcon } from 'lucide-react';
-import { z } from 'zod';
-import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
-import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
-import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment } from '@/app/actions/composio.actions';
-import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit';
-import { ComposioTriggerTypesPanel } from './ComposioTriggerTypesPanel';
-import { TriggerConfigForm } from './TriggerConfigForm';
-import { ToolkitAuthModal } from '../../tools/components/ToolkitAuthModal';
-import { ZToolkit } from "@/src/application/lib/composio/types";
-import { Project } from "@/src/entities/models/project";
-
-interface TriggersModalProps {
- isOpen: boolean;
- onClose: () => void;
- projectId: string;
- projectConfig: z.infer;
- onProjectConfigUpdated?: () => void;
-}
-
-type TriggerDeployment = z.infer;
-
-export function TriggersModal({
- isOpen,
- onClose,
- projectId,
- projectConfig,
- onProjectConfigUpdated,
-}: TriggersModalProps) {
- const [triggers, setTriggers] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [showCreateFlow, setShowCreateFlow] = useState(false);
- const [selectedToolkit, setSelectedToolkit] = useState | null>(null);
- const [selectedTriggerType, setSelectedTriggerType] = useState | null>(null);
- const [showAuthModal, setShowAuthModal] = useState(false);
- const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false);
- const [deletingTrigger, setDeletingTrigger] = useState(null);
-
- const loadTriggers = useCallback(async () => {
- try {
- setLoading(true);
- setError(null);
- const response = await listComposioTriggerDeployments({ projectId });
- setTriggers(response.items);
- } catch (err: any) {
- console.error('Error loading triggers:', err);
- setError('Failed to load triggers. Please try again.');
- } finally {
- setLoading(false);
- }
- }, [projectId]);
-
- const handleDeleteTrigger = async (deploymentId: string) => {
- if (!window.confirm('Are you sure you want to delete this trigger?')) {
- return;
- }
-
- try {
- setDeletingTrigger(deploymentId);
- await deleteComposioTriggerDeployment({ projectId, deploymentId });
- await loadTriggers(); // Reload the list
- } catch (err: any) {
- console.error('Error deleting trigger:', err);
- setError('Failed to delete trigger. Please try again.');
- } finally {
- setDeletingTrigger(null);
- }
- };
-
- const handleCreateNew = () => {
- setShowCreateFlow(true);
- };
-
- const handleBackToList = () => {
- setShowCreateFlow(false);
- setSelectedToolkit(null);
- setSelectedTriggerType(null);
- setShowAuthModal(false);
- setIsSubmittingTrigger(false);
- loadTriggers(); // Reload in case any triggers were created
- };
-
- const handleSelectToolkit = (toolkit: z.infer) => {
- setSelectedToolkit(toolkit);
- };
-
- const handleBackToToolkitSelection = () => {
- setSelectedToolkit(null);
- setSelectedTriggerType(null);
- setIsSubmittingTrigger(false);
- };
-
- const handleSelectTriggerType = (triggerType: z.infer) => {
- if (!selectedToolkit) return;
-
- setSelectedTriggerType(triggerType);
-
- // Check if toolkit requires auth and if connected account exists
- const needsAuth = !selectedToolkit.no_auth;
- const hasConnection = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status === 'ACTIVE';
-
- if (needsAuth && !hasConnection) {
- // Show auth modal
- setShowAuthModal(true);
- } else {
- // Proceed to trigger configuration
- // For now this is just the placeholder, but will be actual config later
- }
- };
-
- const handleAuthComplete = async () => {
- setShowAuthModal(false);
- onProjectConfigUpdated?.();
- };
-
- const handleTriggerSubmit = async (triggerConfig: Record) => {
- if (!selectedToolkit || !selectedTriggerType) return;
-
- try {
- setIsSubmittingTrigger(true);
-
- // Get the connected account ID for this toolkit
- const connectedAccountId = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.id;
-
- if (!connectedAccountId) {
- throw new Error('No connected account found for this toolkit');
- }
-
- // Create the trigger deployment
- await createComposioTriggerDeployment({
- projectId,
- toolkitSlug: selectedToolkit.slug,
- triggerTypeSlug: selectedTriggerType.slug,
- connectedAccountId,
- triggerConfig,
- });
-
- // Success! Go back to triggers list and reload
- handleBackToList();
- } catch (err: any) {
- console.error('Error creating trigger:', err);
- setError('Failed to create trigger. Please try again.');
- } finally {
- setIsSubmittingTrigger(false);
- }
- };
-
- useEffect(() => {
- if (isOpen && !showCreateFlow) {
- loadTriggers();
- }
- }, [isOpen, showCreateFlow, loadTriggers]);
-
- const renderTriggerList = () => {
- if (loading) {
- return (
-
-
- Loading triggers...
-
- );
- }
-
- if (error) {
- return (
-
-
{error}
-
- Try Again
-
-
- );
- }
-
- if (triggers.length === 0) {
- return (
-
-
-
- No triggers configured
-
-
- Set up your first trigger to listen for events from your connected apps.
-
-
}
- onPress={handleCreateNew}
- >
- Create your first trigger
-
-
- );
- }
-
- return (
-
-
-
- Active Triggers ({triggers.length})
-
- }
- onPress={handleCreateNew}
- >
- Create New Trigger
-
-
-
-
- {triggers.map((trigger) => (
-
-
-
-
- {trigger.triggerTypeSlug}
-
-
- Created {new Date(trigger.createdAt).toLocaleDateString()}
-
-
- handleDeleteTrigger(trigger.id)}
- >
-
-
-
-
-
-
Trigger ID: {trigger.triggerId}
-
Connected Account: {trigger.connectedAccountId}
- {Object.keys(trigger.triggerConfig).length > 0 && (
-
-
Configuration:
-
- {JSON.stringify(trigger.triggerConfig, null, 2)}
-
-
- )}
-
-
-
- ))}
-
-
- );
- };
-
- const renderCreateFlow = () => {
- // If trigger type is selected and auth is complete, show config
- if (selectedToolkit && selectedTriggerType && !showAuthModal) {
- const needsAuth = !selectedToolkit.no_auth;
- const hasConnection = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status === 'ACTIVE';
-
- if (!needsAuth || hasConnection) {
- return (
-
- );
- }
- }
-
- // If no toolkit selected, show toolkit selection
- if (!selectedToolkit) {
- return (
-
-
-
- Select a Toolkit to Create Trigger
-
-
- ← Back to Triggers
-
-
-
-
-
- );
- }
-
- // If toolkit selected, show trigger types
- return (
-
-
-
- );
- };
-
- return (
- <>
-
-
-
-
-
- Manage Triggers
-
-
-
- {showCreateFlow ? renderCreateFlow() : renderTriggerList()}
-
- {!showCreateFlow && (
-
-
- Close
-
-
- )}
-
-
-
- {/* Auth Modal */}
- {selectedToolkit && (
- setShowAuthModal(false)}
- toolkitSlug={selectedToolkit.slug}
- projectId={projectId}
- onComplete={handleAuthComplete}
- />
- )}
- >
- );
-}
\ No newline at end of file
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx
index 1ca97033..f677f96d 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx
@@ -37,7 +37,6 @@ import { Button as CustomButton } from "@/components/ui/button";
import { ConfigApp } from "../config/app";
import { InputField } from "@/app/lib/components/input-field";
import { VoiceSection } from "../config/components/voice";
-import { TriggersModal } from "./components/TriggersModal";
import { TopBar } from "./components/TopBar";
enablePatches();
@@ -882,9 +881,6 @@ export function WorkflowEditor({
// Modal state for chat widget configuration
const { isOpen: isChatWidgetModalOpen, onOpen: onChatWidgetModalOpen, onClose: onChatWidgetModalClose } = useDisclosure();
- // Modal state for triggers management
- const { isOpen: isTriggersModalOpen, onOpen: onTriggersModalOpen, onClose: onTriggersModalClose } = useDisclosure();
-
// Project name state
const [localProjectName, setLocalProjectName] = useState(projectConfig.name || '');
const [projectNameError, setProjectNameError] = useState(null);
@@ -1285,7 +1281,6 @@ export function WorkflowEditor({
onRevertToLive={handleRevertToLive}
onToggleCopilot={() => setShowCopilot(!showCopilot)}
onSettingsModalOpen={onSettingsModalOpen}
- onTriggersModalOpen={onTriggersModalOpen}
/>
{/* Content Area */}
@@ -1565,14 +1560,6 @@ export function WorkflowEditor({
*/}
- {/* Triggers Management Modal */}
-
);
diff --git a/apps/rowboat/app/projects/components/build-assistant-section.tsx b/apps/rowboat/app/projects/components/build-assistant-section.tsx
index 7d18c0d3..4528917c 100644
--- a/apps/rowboat/app/projects/components/build-assistant-section.tsx
+++ b/apps/rowboat/app/projects/components/build-assistant-section.tsx
@@ -3,7 +3,7 @@
import { useState, useRef, useEffect } from "react";
import { listTemplates, listProjects } from "@/app/actions/project.actions";
import { createProjectWithOptions, createProjectFromJsonWithOptions, createProjectFromTemplate } from "../lib/project-creation-utils";
-import { useRouter } from 'next/navigation';
+import { useRouter, useSearchParams } from 'next/navigation';
import clsx from 'clsx';
import Image from 'next/image';
import mascotImage from '@/public/mascot.png';
@@ -36,6 +36,8 @@ export function BuildAssistantSection() {
const [selectedTab, setSelectedTab] = useState('new');
const fileInputRef = useRef(null);
const router = useRouter();
+ const searchParams = useSearchParams();
+ const [autoCreateLoading, setAutoCreateLoading] = useState(false);
const totalPages = Math.ceil(projects.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
@@ -103,6 +105,29 @@ export function BuildAssistantSection() {
fetchProjects();
}, []);
+ // Handle URL parameters for auto-creation and direct redirect to build view
+ useEffect(() => {
+ const urlPrompt = searchParams.get('prompt');
+ const urlTemplate = searchParams.get('template');
+
+ if (urlPrompt || urlTemplate) {
+ setAutoCreateLoading(true);
+ createProjectWithOptions({
+ template: urlTemplate || undefined,
+ prompt: urlPrompt || undefined,
+ router,
+ onError: (error) => {
+ console.error('Error auto-creating project:', error);
+ setAutoCreateLoading(false);
+ // Fall back to showing the form with the prompt pre-filled
+ if (urlPrompt) {
+ setUserPrompt(urlPrompt);
+ }
+ }
+ });
+ }
+ }, [searchParams, router]);
+
const handleCreateAssistant = async () => {
setIsCreating(true);
try {
@@ -170,6 +195,15 @@ export function BuildAssistantSection() {
className="hidden"
onChange={handleFileChange}
/>
+ {autoCreateLoading && (
+
+
+
+ Creating your assistant...
+
+
+ )}
+ {!autoCreateLoading && (
{/* Main Headline */}
@@ -445,6 +479,7 @@ export function BuildAssistantSection() {
)}
+ )}
>
);
}
\ No newline at end of file
diff --git a/apps/rowboat/app/projects/layout/components/sidebar.tsx b/apps/rowboat/app/projects/layout/components/sidebar.tsx
index 25085c69..bd1a92c3 100644
--- a/apps/rowboat/app/projects/layout/components/sidebar.tsx
+++ b/apps/rowboat/app/projects/layout/components/sidebar.tsx
@@ -2,7 +2,8 @@
import { useEffect, useState } from 'react';
import Link from "next/link";
import Image from "next/image";
-import logoImage from '@/public/logo-only.png';
+import logo from '@/public/logo.png';
+import logoOnly from '@/public/logo-only.png';
import { usePathname } from "next/navigation";
import { Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
import { UserButton } from "@/app/lib/components/user_button";
@@ -17,7 +18,8 @@ import {
HelpCircle,
MessageSquareIcon,
LogsIcon,
- Clock
+ Clock,
+ ZapIcon
} from "lucide-react";
import { fetchProject } from "@/app/actions/project.actions";
import { createProjectWithOptions } from "../../lib/project-creation-utils";
@@ -102,6 +104,12 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
icon: WorkflowIcon,
requiresProject: true
},
+ {
+ href: 'job-rules',
+ label: 'Triggers',
+ icon: ZapIcon,
+ requiresProject: true
+ },
{
href: 'conversations',
label: 'Conversations',
@@ -114,12 +122,6 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
icon: LogsIcon,
requiresProject: true
},
- {
- href: 'job-rules',
- label: 'Job Rules',
- icon: Clock,
- requiresProject: true
- },
{
href: 'config',
label: 'Settings',
@@ -154,18 +156,17 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
${collapsed ? 'py-3' : 'gap-3 px-4 py-2.5 justify-start'}
`}
>
-
- {!collapsed && (
-
- Rowboat
-
- )}
+ width={24}
+ height={24}
+ />}
+ {!collapsed && }
diff --git a/apps/rowboat/components/common/billing-upgrade-modal.tsx b/apps/rowboat/components/common/billing-upgrade-modal.tsx
index 18c4a208..fe087517 100644
--- a/apps/rowboat/components/common/billing-upgrade-modal.tsx
+++ b/apps/rowboat/components/common/billing-upgrade-modal.tsx
@@ -74,17 +74,18 @@ export function BillingUpgradeModal({ isOpen, onClose, errorMessage }: BillingUp
plan: "starter" as const,
description: "Great for your personal projects",
features: [
- "1000 playground chat requests",
- "500 copilot requests"
+ "2,000 credits",
+ "Latest models like gpt-5, claude-4 and others",
]
},
{
name: "Pro",
plan: "pro" as const,
- description: "Great for enterprise teams",
+ description: "Great for power users or teams",
features: [
- "10000 playground chat requests",
- "2000 copilot requests"
+ "20,000 credits",
+ "o3 and o3-pro",
+ "Priority support",
],
recommended: true
}
diff --git a/apps/rowboat/di/container.ts b/apps/rowboat/di/container.ts
index 294a3158..f2af8a95 100644
--- a/apps/rowboat/di/container.ts
+++ b/apps/rowboat/di/container.ts
@@ -23,6 +23,7 @@ import { MongodbProjectsRepository } from "@/src/infrastructure/repositories/mon
import { MongodbComposioTriggerDeploymentsRepository } from "@/src/infrastructure/repositories/mongodb.composio-trigger-deployments.repository";
import { CreateComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case";
import { ListComposioTriggerDeploymentsUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case";
+import { FetchComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case";
import { DeleteComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case";
import { ListComposioTriggerTypesUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-types.use-case";
import { HandleCompsioWebhookRequestUseCase } from "@/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case";
@@ -30,6 +31,7 @@ import { MongoDBJobsRepository } from "@/src/infrastructure/repositories/mongodb
import { CreateComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller";
import { DeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller";
import { ListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller";
+import { FetchComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller";
import { ListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller";
import { HandleComposioWebhookRequestController } from "@/src/interface-adapters/controllers/composio/webhook/handle-composio-webhook-request.controller";
import { JobsWorker } from "@/src/application/workers/jobs.worker";
@@ -299,10 +301,12 @@ container.register({
listComposioTriggerTypesUseCase: asClass(ListComposioTriggerTypesUseCase).singleton(),
createComposioTriggerDeploymentUseCase: asClass(CreateComposioTriggerDeploymentUseCase).singleton(),
listComposioTriggerDeploymentsUseCase: asClass(ListComposioTriggerDeploymentsUseCase).singleton(),
+ fetchComposioTriggerDeploymentUseCase: asClass(FetchComposioTriggerDeploymentUseCase).singleton(),
deleteComposioTriggerDeploymentUseCase: asClass(DeleteComposioTriggerDeploymentUseCase).singleton(),
createComposioTriggerDeploymentController: asClass(CreateComposioTriggerDeploymentController).singleton(),
deleteComposioTriggerDeploymentController: asClass(DeleteComposioTriggerDeploymentController).singleton(),
listComposioTriggerDeploymentsController: asClass(ListComposioTriggerDeploymentsController).singleton(),
+ fetchComposioTriggerDeploymentController: asClass(FetchComposioTriggerDeploymentController).singleton(),
listComposioTriggerTypesController: asClass(ListComposioTriggerTypesController).singleton(),
// conversations
diff --git a/apps/rowboat/src/application/lib/composio/composio.ts b/apps/rowboat/src/application/lib/composio/composio.ts
index 0b3e8a19..057a14cc 100644
--- a/apps/rowboat/src/application/lib/composio/composio.ts
+++ b/apps/rowboat/src/application/lib/composio/composio.ts
@@ -212,4 +212,9 @@ export async function listTriggersTypes(toolkitSlug: string, cursor?: string): P
// fetch
return composioApiCall(ZListResponse(ZTriggerType), url.toString());
+}
+
+export async function getTriggersType(triggerTypeSlug: string): Promise> {
+ const url = new URL(`${BASE_URL}/triggers_types/${triggerTypeSlug}`);
+ return composioApiCall(ZTriggerType, url.toString());
}
\ No newline at end of file
diff --git a/apps/rowboat/src/application/repositories/composio-trigger-deployments.repository.interface.ts b/apps/rowboat/src/application/repositories/composio-trigger-deployments.repository.interface.ts
index 1bc60e72..a65fd18e 100644
--- a/apps/rowboat/src/application/repositories/composio-trigger-deployments.repository.interface.ts
+++ b/apps/rowboat/src/application/repositories/composio-trigger-deployments.repository.interface.ts
@@ -14,6 +14,7 @@ export const CreateDeploymentSchema = ComposioTriggerDeployment
toolkitSlug: true,
logo: true,
triggerTypeSlug: true,
+ triggerTypeName: true,
triggerConfig: true,
});
diff --git a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts
index 57c804ee..fdd3bbfc 100644
--- a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts
+++ b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts
@@ -2,18 +2,20 @@ import { BadRequestError, NotFoundError } from '@/src/entities/errors/common';
import { z } from "zod";
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
-import { CreateDeploymentSchema, IComposioTriggerDeploymentsRepository } from '../../repositories/composio-trigger-deployments.repository.interface';
+import { IComposioTriggerDeploymentsRepository } from '../../repositories/composio-trigger-deployments.repository.interface';
import { IProjectsRepository } from '../../repositories/projects.repository.interface';
-import { composio, getToolkit } from '../../lib/composio/composio';
+import { composio, getTriggersType } from '../../lib/composio/composio';
import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
const inputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
- data: CreateDeploymentSchema.omit({
- triggerId: true,
- logo: true,
+ projectId: z.string(),
+ data: ComposioTriggerDeployment.pick({
+ triggerTypeSlug: true,
+ connectedAccountId: true,
+ triggerConfig: true,
}),
});
@@ -46,7 +48,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
async execute(request: z.infer): Promise> {
// extract projectid from conversation
- const { projectId } = request.data;
+ const { projectId } = request;
// authz check
await this.projectActionAuthorizationPolicy.authorize({
@@ -59,8 +61,11 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
// assert and consume quota
await this.usageQuotaPolicy.assertAndConsume(projectId);
+ // get trigger type info
+ const triggerType = await getTriggersType(request.data.triggerTypeSlug);
+
// get toolkit info
- const toolkit = await getToolkit(request.data.toolkitSlug);
+ const toolkit = triggerType.toolkit;
// ensure that connected account exists on project
const project = await this.projectsRepository.fetch(projectId);
@@ -69,7 +74,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
}
// ensure connected account exists
- const account = project.composioConnectedAccounts?.[request.data.toolkitSlug];
+ const account = project.composioConnectedAccounts?.[toolkit.slug];
if (!account || account.id !== request.data.connectedAccountId) {
throw new BadRequestError('Invalid connected account');
}
@@ -81,7 +86,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
}
// create trigger on composio
- const result = await composio.triggers.create(request.data.projectId, request.data.triggerTypeSlug, {
+ const result = await composio.triggers.create(projectId, request.data.triggerTypeSlug, {
connectedAccountId: request.data.connectedAccountId,
triggerConfig: request.data.triggerConfig,
});
@@ -89,11 +94,12 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
// create trigger deployment in db
return await this.composioTriggerDeploymentsRepository.create({
projectId,
- toolkitSlug: request.data.toolkitSlug,
- logo: toolkit.meta.logo,
+ toolkitSlug: toolkit.slug,
+ logo: toolkit.logo,
triggerId: result.triggerId,
connectedAccountId: request.data.connectedAccountId,
triggerTypeSlug: request.data.triggerTypeSlug,
+ triggerTypeName: triggerType.name,
triggerConfig: request.data.triggerConfig,
});
}
diff --git a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts
new file mode 100644
index 00000000..ed0be2de
--- /dev/null
+++ b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts
@@ -0,0 +1,62 @@
+import { NotFoundError } from '@/src/entities/errors/common';
+import { z } from "zod";
+import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
+import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
+import { IComposioTriggerDeploymentsRepository } from '../../repositories/composio-trigger-deployments.repository.interface';
+import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
+
+const inputSchema = z.object({
+ caller: z.enum(["user", "api"]),
+ userId: z.string().optional(),
+ apiKey: z.string().optional(),
+ deploymentId: z.string(),
+});
+
+export interface IFetchComposioTriggerDeploymentUseCase {
+ execute(request: z.infer): Promise>;
+}
+
+export class FetchComposioTriggerDeploymentUseCase implements IFetchComposioTriggerDeploymentUseCase {
+ private readonly composioTriggerDeploymentsRepository: IComposioTriggerDeploymentsRepository;
+ private readonly usageQuotaPolicy: IUsageQuotaPolicy;
+ private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
+
+ constructor({
+ composioTriggerDeploymentsRepository,
+ usageQuotaPolicy,
+ projectActionAuthorizationPolicy,
+ }: {
+ composioTriggerDeploymentsRepository: IComposioTriggerDeploymentsRepository,
+ usageQuotaPolicy: IUsageQuotaPolicy,
+ projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
+ }) {
+ this.composioTriggerDeploymentsRepository = composioTriggerDeploymentsRepository;
+ this.usageQuotaPolicy = usageQuotaPolicy;
+ this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
+ }
+
+ async execute(request: z.infer): Promise> {
+ // fetch deployment first to get projectId
+ const deployment = await this.composioTriggerDeploymentsRepository.fetch(request.deploymentId);
+ if (!deployment) {
+ throw new NotFoundError(`Composio trigger deployment ${request.deploymentId} not found`);
+ }
+
+ const { projectId } = deployment;
+
+ // authz check
+ await this.projectActionAuthorizationPolicy.authorize({
+ caller: request.caller,
+ userId: request.userId,
+ apiKey: request.apiKey,
+ projectId,
+ });
+
+ // assert and consume quota
+ await this.usageQuotaPolicy.assertAndConsume(projectId);
+
+ return deployment;
+ }
+}
+
+
diff --git a/apps/rowboat/src/entities/models/composio-trigger-deployment.ts b/apps/rowboat/src/entities/models/composio-trigger-deployment.ts
index b6ccc294..194b6842 100644
--- a/apps/rowboat/src/entities/models/composio-trigger-deployment.ts
+++ b/apps/rowboat/src/entities/models/composio-trigger-deployment.ts
@@ -6,6 +6,7 @@ export const ComposioTriggerDeployment = z.object({
triggerId: z.string(),
toolkitSlug: z.string(),
triggerTypeSlug: z.string(),
+ triggerTypeName: z.string(),
connectedAccountId: z.string(),
triggerConfig: z.record(z.string(), z.unknown()),
logo: z.string(),
diff --git a/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller.ts b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller.ts
index 275fdb32..d8e4c450 100644
--- a/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller.ts
+++ b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller.ts
@@ -2,15 +2,16 @@ import { BadRequestError } from "@/src/entities/errors/common";
import z from "zod";
import { ICreateComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case";
import { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment";
-import { CreateDeploymentSchema } from "@/src/application/repositories/composio-trigger-deployments.repository.interface";
const inputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
- data: CreateDeploymentSchema.omit({
- triggerId: true,
- logo: true,
+ projectId: z.string(),
+ data: ComposioTriggerDeployment.pick({
+ triggerTypeSlug: true,
+ connectedAccountId: true,
+ triggerConfig: true,
}),
});
@@ -35,13 +36,14 @@ export class CreateComposioTriggerDeploymentController implements ICreateComposi
if (!result.success) {
throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`);
}
- const { caller, userId, apiKey, data } = result.data;
+ const { caller, userId, apiKey, projectId, data } = result.data;
// execute use case
return await this.createComposioTriggerDeploymentUseCase.execute({
caller,
userId,
apiKey,
+ projectId,
data,
});
}
diff --git a/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts
new file mode 100644
index 00000000..a630549b
--- /dev/null
+++ b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts
@@ -0,0 +1,44 @@
+import { BadRequestError } from "@/src/entities/errors/common";
+import z from "zod";
+import { IFetchComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case";
+import { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment";
+
+const inputSchema = z.object({
+ caller: z.enum(["user", "api"]),
+ userId: z.string().optional(),
+ apiKey: z.string().optional(),
+ deploymentId: z.string(),
+});
+
+export interface IFetchComposioTriggerDeploymentController {
+ execute(request: z.infer): Promise>;
+}
+
+export class FetchComposioTriggerDeploymentController implements IFetchComposioTriggerDeploymentController {
+ private readonly fetchComposioTriggerDeploymentUseCase: IFetchComposioTriggerDeploymentUseCase;
+
+ constructor({
+ fetchComposioTriggerDeploymentUseCase,
+ }: {
+ fetchComposioTriggerDeploymentUseCase: IFetchComposioTriggerDeploymentUseCase,
+ }) {
+ this.fetchComposioTriggerDeploymentUseCase = fetchComposioTriggerDeploymentUseCase;
+ }
+
+ async execute(request: z.infer): Promise> {
+ const result = inputSchema.safeParse(request);
+ if (!result.success) {
+ throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`);
+ }
+ const { caller, userId, apiKey, deploymentId } = result.data;
+
+ return await this.fetchComposioTriggerDeploymentUseCase.execute({
+ caller,
+ userId,
+ apiKey,
+ deploymentId,
+ });
+ }
+}
+
+