diff --git a/backend/agent/agent_builder_prompt.py b/backend/agent/agent_builder_prompt.py
index a29f8723..e8854bee 100644
--- a/backend/agent/agent_builder_prompt.py
+++ b/backend/agent/agent_builder_prompt.py
@@ -70,18 +70,17 @@ Build structured, repeatable processes:
- **`activate_workflow`**: Enable/disable workflow execution
### ⏰ Trigger Management
-Schedule automatic execution{f''' and event-based triggers''' if config.ENV_MODE != EnvMode.PRODUCTION else ""}:
+Schedule automatic execution and event-based triggers:
- **`create_scheduled_trigger`**: Set up cron-based scheduling
- **`get_scheduled_triggers`**: View all scheduled tasks
- **`delete_scheduled_trigger`**: Remove scheduled tasks
- **`toggle_scheduled_trigger`**: Enable/disable scheduled execution
-{f'''
-Event-based triggers (Composio):
+
+Event/APP-based triggers (Composio):
- **`list_event_trigger_apps`**: Discover apps with available event triggers
- **`list_app_event_triggers`**: List triggers for a specific app (includes config schema)
-- **`list_event_profiles`**: List connected profiles to get `profile_id` and `connected_account_id`
+- **`get_credential_profiles`**: List connected profiles to get `profile_id` and `connected_account_id`
- **`create_event_trigger`**: Create an event trigger by passing `slug`, `profile_id`, `connected_account_id`, `trigger_config`, and route (`agent` or `workflow`). If route is `agent`, pass `agent_prompt`; if `workflow`, pass `workflow_id` (and optional `workflow_input`).
-''' if config.ENV_MODE != EnvMode.PRODUCTION else ""}
### 📊 Agent Management
- **`get_current_agent_config`**: Review current setup and capabilities
diff --git a/backend/agent/tools/agent_builder_tools/credential_profile_tool.py b/backend/agent/tools/agent_builder_tools/credential_profile_tool.py
index 3b75ec9a..09983426 100644
--- a/backend/agent/tools/agent_builder_tools/credential_profile_tool.py
+++ b/backend/agent/tools/agent_builder_tools/credential_profile_tool.py
@@ -48,6 +48,9 @@ class CredentialProfileTool(AgentBuilderBaseTool):
formatted_profiles = []
for profile in profiles:
formatted_profiles.append({
+ "profile_id": profile.profile_id,
+ "connected_account_id": getattr(profile, 'connected_account_id', None),
+ "account_id": profile.account_id,
"profile_name": profile.profile_name,
"display_name": profile.display_name,
"toolkit_slug": profile.toolkit_slug,
diff --git a/backend/agent/tools/agent_builder_tools/trigger_tool.py b/backend/agent/tools/agent_builder_tools/trigger_tool.py
index d8a792b7..aacd138f 100644
--- a/backend/agent/tools/agent_builder_tools/trigger_tool.py
+++ b/backend/agent/tools/agent_builder_tools/trigger_tool.py
@@ -408,10 +408,9 @@ class TriggerTool(AgentBuilderBaseTool):
logger.error(f"Error toggling scheduled trigger: {str(e)}")
return self.fail_response("Error toggling scheduled trigger")
- # ===== EVENT-BASED TRIGGERS (Non-Production Only) =====
+ # ===== EVENT-BASED TRIGGERS =====
-# Event trigger methods - only available in non-production environments
-if config.ENV_MODE != EnvMode.PRODUCTION:
+# Event trigger methods - available in all environments
@openapi_schema({
"type": "function",
"function": {
@@ -443,8 +442,6 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
except Exception as e:
logger.error(f"Error listing event trigger apps: {e}")
return self.fail_response("Error listing apps")
-
- TriggerTool.list_event_trigger_apps = list_event_trigger_apps
@openapi_schema({
"type": "function",
@@ -485,62 +482,6 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
except Exception as e:
logger.error(f"Error listing triggers for app {toolkit_slug}: {e}")
return self.fail_response("Error listing triggers")
-
- TriggerTool.list_app_event_triggers = list_app_event_triggers
-
- @openapi_schema({
- "type": "function",
- "function": {
- "name": "list_event_profiles",
- "description": "List connected Composio profiles for a toolkit. Use this to get profile_id and connected_account_id before creating a trigger.",
- "parameters": {
- "type": "object",
- "properties": {
- "toolkit_slug": {
- "type": "string",
- "description": "Toolkit slug, e.g. 'gmail'"
- }
- },
- "required": ["toolkit_slug"]
- }
- }
- })
- @usage_example('''
-
-
- gmail
-
-
- ''')
- async def list_event_profiles(self, toolkit_slug: str) -> ToolResult:
- try:
- client = await self.db.client
- agent_rows = await client.table('agents').select('account_id').eq('agent_id', self.agent_id).execute()
- if not agent_rows.data:
- return self.fail_response("Agent not found")
- account_id = agent_rows.data[0]['account_id']
-
- profile_service = ComposioProfileService(self.db)
- profiles = await profile_service.get_profiles(account_id, toolkit_slug)
-
- items = []
- for p in profiles:
- items.append({
- "profile_name": p.profile_name,
- "display_name": p.display_name,
- "is_connected": p.is_connected
- })
-
- return self.success_response({
- "message": f"Found {len(items)} profile(s) for {toolkit_slug}",
- "items": items,
- "total": len(items)
- })
- except Exception as e:
- logger.error(f"Error listing event profiles: {e}")
- return self.fail_response("Error listing profiles")
-
- TriggerTool.list_event_profiles = list_event_profiles
@openapi_schema({
"type": "function",
@@ -568,7 +509,9 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
gmail
- gmail
+
+ [toolkit_slug]
+
GMAIL_NEW_GMAIL_MESSAGE
profile_123
@@ -598,15 +541,25 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
if route == "agent" and not agent_prompt:
return self.fail_response("agent_prompt is required when route is 'agent'")
- # Resolve composio user id and connected account id from profile
+ # Get profile config
profile_service = ComposioProfileService(self.db)
- profile_cfg = await profile_service.get_profile_config(profile_id)
- composio_user_id = profile_cfg.get("user_id")
+ try:
+ profile_config = await profile_service.get_profile_config(profile_id)
+ except Exception as e:
+ logger.error(f"Failed to get profile config: {e}")
+ return self.fail_response(f"Failed to get profile config: {str(e)}")
+
+ composio_user_id = profile_config.get("user_id")
if not composio_user_id:
return self.fail_response("Composio profile is missing user_id")
- if not connected_account_id:
- connected_account_id = profile_cfg.get("connected_account_id")
+
+ # Get toolkit_slug and build qualified_name
+ toolkit_slug = profile_config.get("toolkit_slug")
+ if not toolkit_slug and slug:
+ toolkit_slug = slug.split('_')[0].lower() if '_' in slug else 'composio'
+ qualified_name = f'composio.{toolkit_slug}' if toolkit_slug and toolkit_slug != 'composio' else 'composio'
+ # API setup
api_base = os.getenv("COMPOSIO_API_BASE", "https://backend.composio.dev").rstrip("/")
api_key = os.getenv("COMPOSIO_API_KEY")
if not api_key:
@@ -646,47 +599,35 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
coerced_config[key] = ",".join(str(x) for x in val)
elif not isinstance(val, str):
coerced_config[key] = str(val)
- except Exception:
+ except Exception as e:
+ logger.warning(f"Failed to coerce config key {key}: {e}")
pass
- except Exception:
+ except Exception as e:
+ logger.warning(f"Failed to fetch trigger schema: {e}")
pass
- # Upsert trigger instance with webhook
- base_url = os.getenv("WEBHOOK_BASE_URL", "http://localhost:8000").rstrip("/")
- secret = os.getenv("COMPOSIO_WEBHOOK_SECRET", "")
- webhook_headers: Dict[str, str] = {"X-Composio-Secret": secret} if secret else {}
- vercel_bypass = os.getenv("VERCEL_PROTECTION_BYPASS_KEY", "")
- if vercel_bypass:
- webhook_headers["X-Vercel-Protection-Bypass"] = vercel_bypass
-
- body: Dict[str, Any] = {
+ # Build request body (simplified like in API)
+ body = {
"user_id": composio_user_id,
- "userId": composio_user_id,
"trigger_config": coerced_config,
- "triggerConfig": coerced_config,
- "webhook": {
- "url": f"{base_url}/api/composio/webhook",
- "headers": webhook_headers,
- "method": "POST",
- },
}
if connected_account_id:
- body["connectedAccountId"] = connected_account_id
body["connected_account_id"] = connected_account_id
- body["connectedAccountIds"] = [connected_account_id]
- body["connected_account_ids"] = [connected_account_id]
+ # Upsert trigger instance
upsert_url = f"{api_base}/api/v3/trigger_instances/{slug}/upsert"
async with httpx.AsyncClient(timeout=20) as http_client:
resp = await http_client.post(upsert_url, headers=headers, json=body)
try:
resp.raise_for_status()
- except httpx.HTTPStatusError:
+ except httpx.HTTPStatusError as e:
ct = resp.headers.get("content-type", "")
detail = resp.json() if "application/json" in ct else resp.text
- return self.fail_response("Composio upsert error")
+ logger.error(f"Composio upsert error - status: {resp.status_code}, detail: {detail}")
+ return self.fail_response(f"Composio upsert error: {detail}")
created = resp.json()
+ # Extract trigger ID (same logic as API)
def _extract_id(obj: Dict[str, Any]) -> Optional[str]:
if not isinstance(obj, dict):
return None
@@ -700,6 +641,7 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
)
if cand:
return cand
+ # Nested shapes
for k in ("trigger", "trigger_instance", "triggerInstance", "data", "result"):
nested = obj.get(k)
if isinstance(nested, dict):
@@ -714,49 +656,47 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
composio_trigger_id = _extract_id(created) if isinstance(created, dict) else None
- if not composio_trigger_id:
- # fallback to list active
- try:
- params_lookup: Dict[str, Any] = {"limit": 50, "slug": slug, "userId": composio_user_id}
- if connected_account_id:
- params_lookup["connectedAccountId"] = connected_account_id
- list_url = f"{api_base}/api/v3/trigger_instances/active"
- async with httpx.AsyncClient(timeout=15) as http_client:
- lr = await http_client.get(list_url, headers=headers, params=params_lookup)
- if lr.status_code == 200:
- ldata = lr.json()
- items = ldata.get("items") if isinstance(ldata, dict) else (ldata if isinstance(ldata, list) else [])
- if items:
- composio_trigger_id = _extract_id(items[0] if isinstance(items[0], dict) else {})
- except Exception:
- pass
-
if not composio_trigger_id:
return self.fail_response("Failed to get Composio trigger id from response")
-
- # Build Suna trigger and save
+
+ # Build Suna trigger config (same as API)
suna_config: Dict[str, Any] = {
+ "provider_id": "composio",
"composio_trigger_id": composio_trigger_id,
"trigger_slug": slug,
- "execution_type": route,
+ "qualified_name": qualified_name,
+ "execution_type": route if route in ("agent", "workflow") else "agent",
"profile_id": profile_id,
}
- if route == "agent":
+ if suna_config["execution_type"] == "agent":
if agent_prompt:
suna_config["agent_prompt"] = agent_prompt
else:
+ if not workflow_id:
+ return self.fail_response("workflow_id is required for workflow route")
suna_config["workflow_id"] = workflow_id
if workflow_input:
suna_config["workflow_input"] = workflow_input
-
+
+ # Create Suna trigger
trigger_svc = get_trigger_service(self.db)
- trigger = await trigger_svc.create_trigger(
- agent_id=self.agent_id,
- provider_id="composio",
- name=name or slug,
- config=suna_config,
- description=f"Composio event: {slug}"
- )
+ try:
+ trigger = await trigger_svc.create_trigger(
+ agent_id=self.agent_id,
+ provider_id="composio",
+ name=name or slug,
+ config=suna_config,
+ description=f"Composio event: {slug}"
+ )
+ except Exception as e:
+ logger.error(f"Failed to create Suna trigger: {e}")
+ return self.fail_response(f"Failed to create Suna trigger: {str(e)}")
+
+ # Sync triggers to version config
+ try:
+ await self._sync_workflows_to_version_config()
+ except Exception as e:
+ logger.warning(f"Failed to sync triggers to version config: {e}")
message = f"Event trigger '{trigger.name}' created successfully.\n"
message += f"Route: {route}. "
@@ -765,12 +705,6 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
else:
message += "Agent execution configured."
- # Sync triggers to version config
- try:
- await self._sync_workflows_to_version_config()
- except Exception as e:
- logger.warning(f"Failed to sync triggers to version config: {e}")
-
return self.success_response({
"message": message,
"trigger": {
@@ -780,7 +714,5 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
}
})
except Exception as e:
- logger.error(f"Error creating event trigger: {e}", exc_info=True)
- return self.fail_response("Error creating event trigger")
-
- TriggerTool.create_event_trigger = create_event_trigger
+ logger.error(f"Exception in create_event_trigger: {e}", exc_info=True)
+ return self.fail_response(f"Error creating event trigger: {str(e)}")
diff --git a/frontend/src/components/agents/triggers/one-click-integrations.tsx b/frontend/src/components/agents/triggers/one-click-integrations.tsx
index bda48733..1fbad8bb 100644
--- a/frontend/src/components/agents/triggers/one-click-integrations.tsx
+++ b/frontend/src/components/agents/triggers/one-click-integrations.tsx
@@ -171,14 +171,14 @@ export const OneClickIntegrations: React.FC = ({
);
})}
- {config.ENV_MODE !== EnvMode.PRODUCTION && }
+
{configuringSchedule && (