From 5c04ba76cb1f165478ccc70a97c818c78f78a33a Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 19 Jul 2026 20:56:42 +0530 Subject: [PATCH 01/20] added app description to the actions struct --- structs.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/structs.go b/structs.go index 809d471c..3e8af1f1 100755 --- a/structs.go +++ b/structs.go @@ -5732,9 +5732,10 @@ type ActionSummary struct { // AppActionResponse - actions grouped by app type AppActionResponse struct { - AppName string `json:"app_name"` - AppID string `json:"app_id"` - Actions []ActionSummary `json:"actions"` + AppName string `json:"app_name"` + AppDescription string `json:"app_description"` + AppID string `json:"app_id"` + Actions []ActionSummary `json:"actions"` } From 406acf48422ea245179fa24f5f099e7581ba7fb2 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 19 Jul 2026 20:58:45 +0530 Subject: [PATCH 02/20] feat: implement app action response builder and optimize workflow app actions retrieval --- shared.go | 147 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 96 insertions(+), 51 deletions(-) diff --git a/shared.go b/shared.go index 93513537..65bf81b5 100644 --- a/shared.go +++ b/shared.go @@ -37782,6 +37782,79 @@ func getOrgAppSummaries(ctx context.Context, user User) ([]AppSummary, error) { return appSummaries, nil } +func buildAppActionResponses(matchedApps []WorkflowApp) []AppActionResponse { + responses := []AppActionResponse{} + + for _, app := range matchedApps { + if len(app.Actions) == 0 { + continue + } + + appDesc := app.Description + if len(appDesc) > 150 { + appDesc = appDesc[:150] + "..." + } + + appResp := AppActionResponse{ + AppName: app.Name, + AppDescription: appDesc, + AppID: app.ID, + Actions: []ActionSummary{}, + } + + for _, action := range app.Actions { + if len(action.Name) == 0 { + continue + } + + params := []ActionParameter{} + for _, param := range action.Parameters { + if len(param.Name) == 0 { + continue + } + + params = append(params, ActionParameter{ + Name: param.Name, + Required: param.Required, + Description: param.Description, + }) + } + + desc := action.Description + if len(desc) > 100 { + desc = desc[:100] + "..." + } + + appResp.Actions = append(appResp.Actions, ActionSummary{ + Name: action.Name, + Description: desc, + Parameters: params, + }) + } + + if len(appResp.Actions) > 0 { + responses = append(responses, appResp) + } + } + + return responses +} + +func getOrgAppActionSummaries(ctx context.Context, user User) ([]AppActionResponse, error) { + apps, err := GetPrioritizedApps(ctx, user) + if err != nil { + log.Printf("[WARNING] Failed getting apps for agent: %s", err) + return nil, err + } + + maxApps := 150 + if len(apps) > maxApps { + apps = apps[:maxApps] + } + + return buildAppActionResponses(apps), nil +} + func GetOrgAppsSummary(resp http.ResponseWriter, request *http.Request) { cors := HandleCors(resp, request) if cors { @@ -37898,6 +37971,7 @@ func GetWorkflowAppActions(resp http.ResponseWriter, request *http.Request) { if len(actionReq.AppNames) > 0 { for _, appName := range actionReq.AppNames { lowerName := strings.ToLower(strings.TrimSpace(appName)) + foundInOrg := false for _, app := range allApps { if strings.ToLower(app.Name) == lowerName && len(app.ID) > 0 { // Check if already added via ID search @@ -37911,9 +37985,30 @@ func GetWorkflowAppActions(resp http.ResponseWriter, request *http.Request) { if !alreadyAdded { matchedApps = append(matchedApps, app) } + foundInOrg = true break } } + + // If not found in org, search Algolia + if !foundInOrg { + algoliaApp, err := HandleAlgoliaAppSearch(ctx, appName) + if err == nil && algoliaApp.ObjectID != "" { + discoveredApp, err := GetApp(ctx, algoliaApp.ObjectID, user, false) + if err == nil && discoveredApp != nil { + alreadyAdded := false + for _, matched := range matchedApps { + if matched.ID == discoveredApp.ID { + alreadyAdded = true + break + } + } + if !alreadyAdded { + matchedApps = append(matchedApps, *discoveredApp) + } + } + } + } } } @@ -37928,57 +38023,7 @@ func GetWorkflowAppActions(resp http.ResponseWriter, request *http.Request) { } // Build response with actions for each matched app - responses := []AppActionResponse{} - - for _, app := range matchedApps { - if len(app.Actions) == 0 { - if debug { - log.Printf("[DEBUG] Skipping app %s because len(Actions) is 0", app.Name) - } - continue - } - - appResp := AppActionResponse{ - AppName: app.Name, - AppID: app.ID, - Actions: []ActionSummary{}, - } - - // Extract minimal action info - for _, action := range app.Actions { - if len(action.Name) == 0 { - continue - } - - // Build parameter list - params := []ActionParameter{} - for _, param := range action.Parameters { - if len(param.Name) == 0 { - continue - } - - params = append(params, ActionParameter{ - Name: param.Name, - Required: param.Required, - }) - } - - desc := action.Description - if len(desc) > 100 { - desc = desc[:100] + "..." - } - - appResp.Actions = append(appResp.Actions, ActionSummary{ - Name: action.Name, - Description: desc, - Parameters: params, - }) - } - - if len(appResp.Actions) > 0 { - responses = append(responses, appResp) - } - } + responses := buildAppActionResponses(matchedApps) if len(responses) == 0 { if debug { From 85eaf54c7cd77f4acd4d96543daf2daf5961a433 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 19 Jul 2026 22:01:47 +0530 Subject: [PATCH 03/20] feat: enhance AgentWorkflowEditor to validate workflow ownership and improve error handling --- shared.go | 139 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 89 insertions(+), 50 deletions(-) diff --git a/shared.go b/shared.go index 65bf81b5..8be33be0 100644 --- a/shared.go +++ b/shared.go @@ -38182,19 +38182,38 @@ func AgentWorkflowEditor(resp http.ResponseWriter, request *http.Request) { return } - // Get org apps summary - appSummaries, err := getOrgAppSummaries(ctx, user) + workflow, err := GetWorkflow(ctx, req.WorkflowId) if err != nil { - log.Printf("[WARNING] Failed getting apps in AgentWorkflowEditor for user %s: %s", user.Username, err) + log.Printf("[WARNING] Failed getting workflow %s in AgentWorkflowEditor: %s", req.WorkflowId, err) + resp.WriteHeader(404) + resp.Write([]byte(`{"success": false, "reason": "Workflow not found"}`)) + return + } + + if user.Id != workflow.Owner && workflow.OrgId != user.ActiveOrg.Id { + log.Printf("[WARNING] User %s (%s) unauthorized to edit workflow %s (owner: %s, org: %s)", + user.Username, user.Id, req.WorkflowId, workflow.Owner, workflow.OrgId) + resp.WriteHeader(403) + resp.Write([]byte(`{"success": false, "reason": "Unauthorized"}`)) + return + } + + minimalWorkflow := buildMinimalWorkflow(workflow) + minimalWorkflowJson, _ := json.Marshal(minimalWorkflow) + + // Get org apps action summaries + appActionSummaries, err := getOrgAppActionSummaries(ctx, user) + if err != nil { + log.Printf("[WARNING] Failed getting app actions in AgentWorkflowEditor for user %s: %s", user.Username, err) resp.WriteHeader(500) resp.Write([]byte(`{"success": false}`)) return } // Build system prompt with apps context - appsJson, _ := json.Marshal(appSummaries) + appsJson, _ := json.Marshal(appActionSummaries) - systemPrompt := fmt.Sprintf(`You are an autonomous workflow-building agent. You build or edit workflows that will eventually automates the task. Only use the special actions build for you, like get_minimal_workflow, agent_update are the ones you have to use. + systemPrompt := fmt.Sprintf(`You are an autonomous workflow-building agent. You build or edit workflows that will eventually automates the task. Only use the special actions build for you, like agent_update are the ones you have to use. If the user requests an integration that is not in your current context, you can still fetch its actions by passing the requested app name to your get_workflow_app_actions tool. The system will automatically search for the app and return its actions. The workflow you are currently working on has the ID: %s — always use this as the "workflow_id" in every payload you send. @@ -38302,81 +38321,101 @@ To use data from another node, reference its label. CRITICAL RULES FOR THE AGENT 1. Handling Errors: If you make a mistake, the API will reject your request and tell you EXACTLY which operation failed and why (e.g., Operation 1 failed: app_id is required). Read this error carefully, fix the specific missing or incorrect parameter, and try again. -2. Special actions are built exculsively for you. get_minimal_workflow is enough to the info about how the workflow is structured and generally there is no need of other meta data info of the worklfow as often times its not that useful. agent_update is another beautiful action that do a lot of heavy lifting for you. +2. Special actions are built exclusively for you. The current minimal workflow state is provided below. agent_update is another beautiful action that does a lot of heavy lifting for you. 3. Try not to use parallel decision calling as much as possible; always do sequential tool calls. + +%s + + %s %s -`, req.WorkflowId, req.WorkflowId, string(appsJson), req.Input) +`, req.WorkflowId, req.WorkflowId, string(minimalWorkflowJson), string(appsJson), req.Input) + + // Build the Action with the right parameters for HandleAiAgentExecutionStart + toolApps := "app:7db43ccd25261967b095cfbd467a75cc:shuffle_apps,app:b598b078fd5c531699fca803c172ce72:shuffle_workflows" + + action := Action{ + ID: uuid.NewV4().String(), + Name: "agent", + AppName: "AI Agent", + AppID: "shuffle_agent", + AppVersion: "1.0.0", + Environment: "cloud", + Parameters: []WorkflowAppActionParameter{ + { + Name: "input", + Value: systemPrompt, + }, + { + Name: "action", + Value: toolApps, + }, + }, + } - // Build MCPRequest with the full system prompt as input - mcpReq := MCPRequest{} - mcpReq.Jsonrpc = "2.0" - mcpReq.Method = "tools/call" - mcpReq.Params.ToolName = "app:9f05339c05f9aaca4eeb35e6f13e41e6:shuffles_app_management,app:b598b078fd5c531699fca803c172ce72:shuffle_workflows" - mcpReq.Params.Input.Text = systemPrompt + workflowId := uuid.NewV4().String() + action.SourceWorkflow = workflowId - mcpBody, err := json.Marshal(mcpReq) - if err != nil { - log.Printf("[ERROR] Failed marshalling MCPRequest in AgentWorkflowEditor: %s", err) - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false}`)) - return + exec := WorkflowExecution{ + Workflow: Workflow{ + ID: workflowId, + Actions: []Action{ + action, + }, + OrgId: user.ActiveOrg.Id, + Owner: user.Username, + UpdatedBy: user.Username, + Start: action.ID, + }, + Type: "AGENT", + Start: action.ID, + Status: "EXECUTING", + WorkflowId: workflowId, + ExecutionId: workflowId, + ExecutionOrg: user.ActiveOrg.Id, + StartedAt: int64(time.Now().Unix()), + Authorization: uuid.NewV4().String(), } - // Call /api/v1/agent directly - backendUrl := os.Getenv("BASE_URL") - if len(backendUrl) == 0 { - if len(os.Getenv("SHUFFLE_CLOUDRUN_URL")) > 0 { - backendUrl = os.Getenv("SHUFFLE_CLOUDRUN_URL") - } else { - port := os.Getenv("PORT") - if len(port) == 0 { - port = "5001" - } - backendUrl = fmt.Sprintf("http://localhost:%s", port) - } - } - - agentReq, err := http.NewRequest("POST", fmt.Sprintf("%s/api/v1/agent", backendUrl), strings.NewReader(string(mcpBody))) + SetWorkflowExecution(ctx, exec, true) + + log.Printf("[INFO] AgentWorkflowEditor: calling HandleAiAgentExecutionStart directly for user %s (%s), workflow_id=%s, execution_id=%s, apps=%d", user.Username, user.Id, req.WorkflowId, exec.ExecutionId, len(appActionSummaries)) + + returnAction, err := HandleAiAgentExecutionStart(exec, action, false, "AgentWorkflowEditor") if err != nil { - log.Printf("[ERROR] Failed creating agent request in AgentWorkflowEditor: %s", err) + log.Printf("[ERROR] HandleAiAgentExecutionStart failed in AgentWorkflowEditor: %s", err) resp.WriteHeader(500) - resp.Write([]byte(`{"success": false}`)) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err.Error()))) return } - agentReq.Header = request.Header.Clone() - agentReq.Header.Set("Content-Type", "application/json") - agentReq.Header.Set("X-Internal-Caller", "AgentWorkflowEditor") - - log.Printf("[INFO] AgentWorkflowEditor: calling /api/v1/agent for user %s (%s), workflow_id=%s, apps=%d", user.Username, user.Id, req.WorkflowId, len(appSummaries)) - - client := &http.Client{} - agentResp, err := client.Do(agentReq) + // Fetch the updated execution to return + newExec, err := GetWorkflowExecution(ctx, exec.ExecutionId) if err != nil { - log.Printf("[ERROR] Failed calling /api/v1/agent in AgentWorkflowEditor: %s", err) + log.Printf("[ERROR] Failed to get workflow execution after agent start in AgentWorkflowEditor: %s", err) resp.WriteHeader(500) resp.Write([]byte(`{"success": false}`)) return } - defer agentResp.Body.Close() - agentRespBody, err := ioutil.ReadAll(agentResp.Body) + _ = returnAction + + responseData, err := json.Marshal(newExec) if err != nil { - log.Printf("[ERROR] Failed reading agent response in AgentWorkflowEditor: %s", err) + log.Printf("[ERROR] Failed marshalling execution response in AgentWorkflowEditor: %s", err) resp.WriteHeader(500) resp.Write([]byte(`{"success": false}`)) return } resp.Header().Set("Content-Type", "application/json") - resp.WriteHeader(agentResp.StatusCode) - resp.Write(agentRespBody) + resp.WriteHeader(200) + resp.Write(responseData) } func generateNodeID() string { From aeee62d08e6b760b66173f1a3c2aa10bf20289c7 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 19 Jul 2026 22:10:37 +0530 Subject: [PATCH 04/20] change it back to mcp format --- shared.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared.go b/shared.go index 8be33be0..c164bfa7 100644 --- a/shared.go +++ b/shared.go @@ -38334,7 +38334,7 @@ CRITICAL RULES FOR THE AGENT %s -`, req.WorkflowId, req.WorkflowId, string(minimalWorkflowJson), string(appsJson), req.Input) +`, req.Params.Input.WorkflowId, req.Params.Input.WorkflowId, string(minimalWorkflowJson), string(appsJson), req.Params.Input.Text) // Build the Action with the right parameters for HandleAiAgentExecutionStart toolApps := "app:7db43ccd25261967b095cfbd467a75cc:shuffle_apps,app:b598b078fd5c531699fca803c172ce72:shuffle_workflows" From aa5ded2c21612d7e0880cbe495a3142a8b41757f Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 19 Jul 2026 22:10:55 +0530 Subject: [PATCH 05/20] change it back to mcp format again --- shared.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared.go b/shared.go index c164bfa7..d0841896 100644 --- a/shared.go +++ b/shared.go @@ -38384,7 +38384,7 @@ CRITICAL RULES FOR THE AGENT SetWorkflowExecution(ctx, exec, true) - log.Printf("[INFO] AgentWorkflowEditor: calling HandleAiAgentExecutionStart directly for user %s (%s), workflow_id=%s, execution_id=%s, apps=%d", user.Username, user.Id, req.WorkflowId, exec.ExecutionId, len(appActionSummaries)) + log.Printf("[INFO] AgentWorkflowEditor: calling HandleAiAgentExecutionStart directly for user %s (%s), workflow_id=%s, execution_id=%s, apps=%d", user.Username, user.Id, req.Params.Input.WorkflowId, exec.ExecutionId, len(appActionSummaries)) returnAction, err := HandleAiAgentExecutionStart(exec, action, false, "AgentWorkflowEditor") if err != nil { From 943f002c2751d1106e6e4bf6582f53d276d1b0bc Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Sun, 19 Jul 2026 22:28:03 +0530 Subject: [PATCH 06/20] updated workflow ID references in accordance with the mcp struct --- shared.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shared.go b/shared.go index 0dbafc66..c07f9d4f 100644 --- a/shared.go +++ b/shared.go @@ -38188,17 +38188,16 @@ func AgentWorkflowEditor(resp http.ResponseWriter, request *http.Request) { return } - workflow, err := GetWorkflow(ctx, req.WorkflowId) + workflow, err := GetWorkflow(ctx, req.Params.Input.WorkflowId) if err != nil { - log.Printf("[WARNING] Failed getting workflow %s in AgentWorkflowEditor: %s", req.WorkflowId, err) + log.Printf("[WARNING] Failed getting workflow %s in AgentWorkflowEditor: %s", req.Params.Input.WorkflowId, err) resp.WriteHeader(404) resp.Write([]byte(`{"success": false, "reason": "Workflow not found"}`)) return } if user.Id != workflow.Owner && workflow.OrgId != user.ActiveOrg.Id { - log.Printf("[WARNING] User %s (%s) unauthorized to edit workflow %s (owner: %s, org: %s)", - user.Username, user.Id, req.WorkflowId, workflow.Owner, workflow.OrgId) + log.Printf("[WARNING] User %s (%s) unauthorized to edit workflow %s (owner: %s, org: %s)", user.Username, user.Id, req.Params.Input.WorkflowId, workflow.Owner, workflow.OrgId) resp.WriteHeader(403) resp.Write([]byte(`{"success": false, "reason": "Unauthorized"}`)) return From 4f3c5e9ab9e5a828570646424d5c2a86f7b76e69 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Mon, 20 Jul 2026 23:29:43 +0530 Subject: [PATCH 07/20] feat: update HandleAiAgentExecutionStart calls and enhance error logging in AgentWorkflowEditor --- shared.go | 218 +++++++++-------------------------------------------- structs.go | 6 ++ 2 files changed, 41 insertions(+), 183 deletions(-) diff --git a/shared.go b/shared.go index c07f9d4f..a8a8c978 100644 --- a/shared.go +++ b/shared.go @@ -18194,7 +18194,7 @@ func handleAgentDecisionStreamResult(workflowExecution WorkflowExecution, action } callerName := "handleAgentDecisionStreamResult" - returnAction, err := HandleAiAgentExecutionStart(workflowExecution, originalAction, true, callerName) + returnAction, err := HandleAiAgentExecutionStart(workflowExecution, originalAction, true, callerName, "") if err != nil { log.Printf("[ERROR][%s] Failed handling agent execution start: %s", workflowExecution.ExecutionId, err) } @@ -18307,7 +18307,7 @@ func ParsedExecutionResult(ctx context.Context, workflowExecution WorkflowExecut if err != nil { log.Printf("[ERROR] AI Agent (10): Failed marshalling actionResult: %s", err) } else { - go HandleAiAgentExecutionStart(*foundParentExec, startNode, false, callerName, marshalledResult) + go HandleAiAgentExecutionStart(*foundParentExec, startNode, false, callerName, "", marshalledResult) } } } else { @@ -22503,7 +22503,7 @@ func PrepareSingleAction(ctx context.Context, parentRequest *http.Request, user log.Printf("[INFO][%s] AI Agent: %s Started standalone for org %s, execution id %s, workflow %s", exec.ExecutionId, callerName, user.ActiveOrg.Id, exec.ExecutionId, exec.WorkflowId) - action, err := HandleAiAgentExecutionStart(exec, action, false, callerName) + action, err := HandleAiAgentExecutionStart(exec, action, false, callerName, "") if err != nil { log.Printf("[ERROR] Failed to handle AI agent execution start: %s", err) } @@ -38174,20 +38174,17 @@ func AgentWorkflowEditor(resp http.ResponseWriter, request *http.Request) { return } - //type agentContextRequest struct { - // Input string `json:"input"` - // WorkflowId string `json:"workflow_id"` - //} - var req MCPRequest err = json.Unmarshal(body, &req) + if err != nil || len(strings.TrimSpace(req.Params.Input.Text)) == 0 { - log.Printf("[WARNING] Bad body in AgentWorkflowEditor: %s", err) + log.Printf("[WARNING] Bad body in AgentWorkflowEditor. Error: %v, Body: %s", err, string(body)) resp.WriteHeader(400) resp.Write([]byte(`{"success": false, "reason": "input field is required"}`)) return } + // Auth check: verify user has access to the target workflow before kicking off the agent workflow, err := GetWorkflow(ctx, req.Params.Input.WorkflowId) if err != nil { log.Printf("[WARNING] Failed getting workflow %s in AgentWorkflowEditor: %s", req.Params.Input.WorkflowId, err) @@ -38203,145 +38200,8 @@ func AgentWorkflowEditor(resp http.ResponseWriter, request *http.Request) { return } - minimalWorkflow := buildMinimalWorkflow(workflow) - minimalWorkflowJson, _ := json.Marshal(minimalWorkflow) - - // Get org apps action summaries - appActionSummaries, err := getOrgAppActionSummaries(ctx, user) - if err != nil { - log.Printf("[WARNING] Failed getting app actions in AgentWorkflowEditor for user %s: %s", user.Username, err) - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Build system prompt with apps context - appsJson, _ := json.Marshal(appActionSummaries) - - systemPrompt := fmt.Sprintf(`You are an autonomous workflow-building agent. You build or edit workflows that will eventually automates the task. Only use the special actions build for you, like agent_update are the ones you have to use. If the user requests an integration that is not in your current context, you can still fetch its actions by passing the requested app name to your get_workflow_app_actions tool. The system will automatically search for the app and return its actions. - -The workflow you are currently working on has the ID: %s — always use this as the "workflow_id" in every payload you send. - -Only ask the user for information when it is genuinely unavailable through any allowed action. - -Payload Structure - -Send your incremental steps in the operations array. You can send one operation at a time, or multiple if you are building a connected sequence. -{ -"workflow_id": "%s", -"operations": [ ...array of step-by-step operations... ] // api just supports bulk operations but its optional, just mention 1 op at a time in the array -} - -Supported Step-by-Step Operations - -A workflow has two types of nodes: Actions and Triggers. If you need a trigger, you use either Webhook or Schedule. Keep in mind that there is no rigid rule that triggers must exists everytime. Only use trigger if the workflow solution you are building really needs it. - -Format: Webhook Trigger - -{ -"op": "add_node", -"node_type": "trigger", -"temp_id": "", -"data": { -"app_name": "Webhook", -"label": "", -"x": 100, -"y": 100 -} -} - -Format: Schedule Trigger - -{ -"op": "add_node", -"node_type": "trigger", -"temp_id": "", -"data": { -"app_name": "Schedule", -"label": "", -"parameters": [ -{ "name": "cron", "value": "*/15 * * * *" } -], -"x": 100, -"y": 100 -} -} - -1. ADDING A NODE (Action) -{ -"op": "add_node", -"node_type": "action", -"temp_id": "a_temp_id_string", -"data": { -"app_id": "", -"action_name": "", -"label": "Authenticate User", -"parameters": [ -{ "name": "param_1", "value": "some_value" } -], -"x": 100, -"y": 100 -} -} -Note: You can vertically position the node between existing ones by adding "insert_before": "" or "insert_after": "" alongside the data field. -2. CONNECTING NODES (Adding a Branch) -Connect your nodes using their real IDs (if they already exist) or the temp_id you assigned when creating them in the same payload. -{ -"op": "add_branch", -"data": { -"source_id": "", -"destination_id": "" -} -} -3. EDITING A NODE -Only provide the fields you actually want to change. -{ -"op": "edit_node", -"id": "", -"data": { -"label": "New Name", -"parameters": [ -{ "name": "param_1", "value": "new_value" } -] -} -} -4. DELETING OR MOVING -{ "op": "delete_node", "node_type": "action", "id": "" } -{ "op": "delete_branch", "id": "" } -{ "op": "move_node", "id": "", "data": { "x": 250, "y": 300 } } -5. SETTING THE START NODE -Defines the entry point of the workflow. You can use a real ID or a temp_id from the same payload. -{ -"op": "set_start_node", -"id": "" -} -6. SAVING THE WORKFLOW -Your edits are stored as a real-time draft. When you are completely finished building or modifying the workflow, you MUST append this operation to your final payload to permanently save the workflow to the database. Make sure you do this after fully finishing all the changes you want to do to the workflow, but don't forget to call it. -{ -"op": "save_workflow" -} - -To use data from another node, reference its label. - -CRITICAL RULES FOR THE AGENT - -1. Handling Errors: If you make a mistake, the API will reject your request and tell you EXACTLY which operation failed and why (e.g., Operation 1 failed: app_id is required). Read this error carefully, fix the specific missing or incorrect parameter, and try again. -2. Special actions are built exclusively for you. The current minimal workflow state is provided below. agent_update is another beautiful action that does a lot of heavy lifting for you. -3. Try not to use parallel decision calling as much as possible; always do sequential tool calls. - - -%s - - - -%s - - - -%s -`, req.Params.Input.WorkflowId, req.Params.Input.WorkflowId, string(minimalWorkflowJson), string(appsJson), req.Params.Input.Text) - - // Build the Action with the right parameters for HandleAiAgentExecutionStart + // All context building (workflow state, app actions, rules) is handled inside + // buildWorkflowEditContext which is called by getTemplateContext inside HandleAiAgentExecutionStart toolApps := "app:7db43ccd25261967b095cfbd467a75cc:shuffle_apps,app:b598b078fd5c531699fca803c172ce72:shuffle_workflows" action := Action{ @@ -38354,7 +38214,7 @@ CRITICAL RULES FOR THE AGENT Parameters: []WorkflowAppActionParameter{ { Name: "input", - Value: systemPrompt, + Value: req.Params.Input.Text, }, { Name: "action", @@ -38377,21 +38237,23 @@ CRITICAL RULES FOR THE AGENT UpdatedBy: user.Username, Start: action.ID, }, - Type: "AGENT", - Start: action.ID, - Status: "EXECUTING", - WorkflowId: workflowId, - ExecutionId: workflowId, - ExecutionOrg: user.ActiveOrg.Id, - StartedAt: int64(time.Now().Unix()), - Authorization: uuid.NewV4().String(), + Type: "AGENT", + Start: action.ID, + Status: "EXECUTING", + WorkflowId: workflowId, + ExecutionId: workflowId, + ExecutionOrg: user.ActiveOrg.Id, + StartedAt: int64(time.Now().Unix()), + Authorization: uuid.NewV4().String(), + // Store the target workflow_id here so buildWorkflowEditContext can fetch it + ExecutionArgument: req.Params.Input.WorkflowId, } SetWorkflowExecution(ctx, exec, true) - log.Printf("[INFO] AgentWorkflowEditor: calling HandleAiAgentExecutionStart directly for user %s (%s), workflow_id=%s, execution_id=%s, apps=%d", user.Username, user.Id, req.Params.Input.WorkflowId, exec.ExecutionId, len(appActionSummaries)) + log.Printf("[INFO] AgentWorkflowEditor: calling HandleAiAgentExecutionStart for user %s (%s), target_workflow_id=%s, execution_id=%s", user.Username, user.Id, req.Params.Input.WorkflowId, exec.ExecutionId) - returnAction, err := HandleAiAgentExecutionStart(exec, action, false, "AgentWorkflowEditor") + returnAction, err := HandleAiAgentExecutionStart(exec, action, false, "AgentWorkflowEditor", "workflow-edit") if err != nil { log.Printf("[ERROR] HandleAiAgentExecutionStart failed in AgentWorkflowEditor: %s", err) resp.WriteHeader(500) @@ -38410,7 +38272,13 @@ CRITICAL RULES FOR THE AGENT _ = returnAction - responseData, err := json.Marshal(newExec) + respObj := agentResponse{ + Success: true, + ExecutionId: newExec.ExecutionId, + Authorization: newExec.Authorization, + } + + responseData, err := json.Marshal(respObj) if err != nil { log.Printf("[ERROR] Failed marshalling execution response in AgentWorkflowEditor: %s", err) resp.WriteHeader(500) @@ -38613,7 +38481,7 @@ func enrichTriggerFromApp(minTrig *MinimalTrigger, environment string) (Trigger, } } -func HandleAgentWorkflowSave(resp http.ResponseWriter, request *http.Request) { +func HandleAgentWorkflowOperations(resp http.ResponseWriter, request *http.Request) { cors := HandleCors(resp, request) if cors { return @@ -38622,7 +38490,7 @@ func HandleAgentWorkflowSave(resp http.ResponseWriter, request *http.Request) { ctx := GetContext(request) user, userErr := HandleApiAuthentication(resp, request) if userErr != nil { - log.Printf("[WARNING] Api authentication failed in HandleAgentWorkflowSave: %s", userErr) + log.Printf("[WARNING] Api authentication failed in HandleAgentWorkflowOperations: %s", userErr) resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": "Authentication failed"}`)) return @@ -38635,17 +38503,6 @@ func HandleAgentWorkflowSave(resp http.ResponseWriter, request *http.Request) { return } - // Extract workflow ID from URL - location := strings.Split(request.URL.String(), "/") - var urlWorkflowID string - if len(location) > 4 && location[1] == "api" { - urlWorkflowID = location[4] - if strings.Contains(urlWorkflowID, "?") { - urlWorkflowID = strings.Split(urlWorkflowID, "?")[0] - } - } - - // Parse request first so we can fallback to body's WorkflowID body, err := ioutil.ReadAll(request.Body) if err != nil { log.Printf("[WARNING] Failed reading request body: %s", err) @@ -38664,17 +38521,12 @@ func HandleAgentWorkflowSave(resp http.ResponseWriter, request *http.Request) { return } - workflowID := urlWorkflowID + workflowID := setOpsReq.WorkflowID if len(workflowID) != 36 { - // Fallback to body's WorkflowID if URL ID is invalid (e.g., %7Bkey%7D from MCP) - if len(setOpsReq.WorkflowID) == 36 { - workflowID = setOpsReq.WorkflowID - } else { - log.Printf("[WARNING] Invalid workflow ID: %s", urlWorkflowID) - resp.WriteHeader(400) - resp.Write([]byte(`{"success": false, "reason": "Invalid workflow ID"}`)) - return - } + log.Printf("[WARNING] Invalid or missing workflow ID in body: %s", workflowID) + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "Invalid workflow ID"}`)) + return } // Validate request diff --git a/structs.go b/structs.go index a0d06a1a..3f042860 100755 --- a/structs.go +++ b/structs.go @@ -5785,3 +5785,9 @@ type rawField struct { Name string `json:"name"` Value interface{} `json:"value"` } + +type agentResponse struct { + Success bool `json:"success"` + ExecutionId string `json:"execution_id"` + Authorization string `json:"authorization"` +} \ No newline at end of file From 7a8d8b648129d78462844d679d2efa00df1705fb Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Mon, 20 Jul 2026 23:34:56 +0530 Subject: [PATCH 08/20] feat: enhance HandleAiAgentExecutionStart call with template parameter in fixexecution function --- ai.go | 207 ++++++++++++++++++++++++++++++++++++++++++++---- db-connector.go | 2 +- 2 files changed, 193 insertions(+), 16 deletions(-) diff --git a/ai.go b/ai.go index 80346cf0..5b25f72f 100644 --- a/ai.go +++ b/ai.go @@ -7644,7 +7644,173 @@ func ReduceAgentResponseData(rawResponse []byte, dataFilter string, fieldsNeeded // createNextActions = false => start of agent to find initial decisions // createNextActions = true => mid-agent to decide next steps -func HandleAiAgentExecutionStart(execution WorkflowExecution, startNode Action, createNextActions bool, callerName string, aiResponseWrapper ...[]byte) (Action, error) { + +func getTemplateContext(ctx context.Context, template string, execution WorkflowExecution) (string, string, error) { + switch template { + case "workflow-edit": + return buildWorkflowEditContext(ctx, execution) + default: + return "", "", nil + } +} + +func buildWorkflowEditContext(ctx context.Context, execution WorkflowExecution) (string, string, error) { + targetWorkflowId := execution.ExecutionArgument + + user := User{ + ActiveOrg: OrgMini{Id: execution.ExecutionOrg}, + } + appSummaries, err := getOrgAppSummaries(ctx, user) + if err != nil { + log.Printf("[WARNING] buildWorkflowEditContext: failed getting app summaries for org %s: %s", execution.ExecutionOrg, err) + } + appsJson, _ := json.Marshal(appSummaries) + + // Workflow state section, skip fetch if creating from scratch + workflowStateSection := "\n(No existing workflow — you are creating one from scratch. Use create_workflow first, then work on it.)\n" + if len(targetWorkflowId) > 0 { + workflow, wErr := GetWorkflow(ctx, targetWorkflowId) + if wErr != nil { + log.Printf("[WARNING] buildWorkflowEditContext: failed to get workflow %s: %s", targetWorkflowId, wErr) + } else { + minimalWorkflow := buildMinimalWorkflow(workflow) + minimalWorkflowJson, _ := json.Marshal(minimalWorkflow) + workflowStateSection = fmt.Sprintf("\n%s\n", string(minimalWorkflowJson)) + } + } + + // What we tell the agent about its workflow_id + workflowIdLine := "You do NOT have an existing workflow yet. Your FIRST action MUST be to call create_workflow to create one, then use the returned workflow_id for all subsequent operations." + if len(targetWorkflowId) > 0 { + workflowIdLine = fmt.Sprintf(`The workflow you are currently working on has the ID: %s — always use this as the "workflow_id" in every payload you send.`, targetWorkflowId) + } + + systemRule := fmt.Sprintf(`[SECONDARY]: You are an autonomous workflow-building agent. You build or edit workflows that will eventually automate the task. Only use the special actions built for you — agent_update is the primary one you will use. If the user requests an integration that is not in your current context, you can still fetch its actions by passing the requested app name to your get_workflow_app_actions tool. The system will automatically search for the app and return its actions. + +%s + +Only ask the user for information when it is genuinely unavailable through any allowed action. Use get_minimal_action to fetch the latest workflow state whenever you need to inspect or verify what is currently in the workflow — prefer this over relying on stale context. + +Payload Structure + +Send your incremental steps in the operations array. You can send one operation at a time, or multiple if you are building a connected sequence. +{ +"workflow_id": "", +"operations": [ ...array of step-by-step operations... ] +} + +Supported Step-by-Step Operations + +A workflow has two types of nodes: Actions and Triggers. If you need a trigger, use either Webhook or Schedule. There is no rigid rule that triggers must always exist — only use a trigger if the workflow solution genuinely needs it. + +Format: Webhook Trigger +{ +"op": "add_node", +"node_type": "trigger", +"temp_id": "", +"data": { + "app_name": "Webhook", + "label": "", + "x": 100, + "y": 100 +} +} + +Format: Schedule Trigger +{ +"op": "add_node", +"node_type": "trigger", +"temp_id": "", +"data": { + "app_name": "Schedule", + "label": "", + "parameters": [ + { "name": "cron", "value": "*/15 * * * *" } + ], + "x": 100, + "y": 100 +} +} + +1. ADDING A NODE (Action) +{ +"op": "add_node", +"node_type": "action", +"temp_id": "a_temp_id_string", +"data": { + "app_id": "", + "action_name": "", + "label": "Authenticate User", + "parameters": [ + { "name": "param_1", "value": "some_value" } + ], + "x": 100, + "y": 100 +} +} +Note: You can vertically position the node between existing ones by adding "insert_before": "" or "insert_after": "" alongside the data field. + +2. CONNECTING NODES (Adding a Branch) +Connect your nodes using their real IDs (if they already exist) or the temp_id you assigned when creating them in the same payload. +{ +"op": "add_branch", +"data": { + "source_id": "", + "destination_id": "" +} +} + +3. EDITING A NODE +Only provide the fields you actually want to change. +{ +"op": "edit_node", +"id": "", +"data": { + "label": "New Name", + "parameters": [ + { "name": "param_1", "value": "new_value" } + ] +} +} + +4. DELETING OR MOVING +{ "op": "delete_node", "node_type": "action", "id": "" } +{ "op": "delete_branch", "id": "" } +{ "op": "move_node", "id": "", "data": { "x": 250, "y": 300 } } + +5. SETTING THE START NODE +Defines the entry point of the workflow. You can use a real ID or a temp_id from the same payload. +{ +"op": "set_start_node", +"id": "" +} + +6. SAVING THE WORKFLOW +Your edits are stored as a real-time draft. When you are completely finished building or modifying the workflow, you MUST append this operation to your final payload to permanently save the workflow to the database. Call this only after all changes are complete — do not forget it. +{ +"op": "save_workflow" +} + +To use data from another node, reference its label. + +CRITICAL RULES FOR THE AGENT + +1. Handling Errors: If you make a mistake, the API will reject your request and tell you EXACTLY which operation failed and why (e.g., "Operation 1 failed: app_id is required"). Read the error carefully, fix the specific missing or incorrect field, and try again. +2. Special actions are built exclusively for you. agent_update is the core action that does most of the heavy lifting. +3. Use get_minimal_action to fetch the current workflow state whenever you need to verify what is there — do not assume the context is fresh. +4. Try not to use parallel decision calling; always do sequential tool calls.`, workflowIdLine) + + templateContext := fmt.Sprintf(`%s + + +%s +`, workflowStateSection, string(appsJson)) + + return systemRule, templateContext, nil +} + + +func HandleAiAgentExecutionStart(execution WorkflowExecution, startNode Action, createNextActions bool, callerName string, template string, aiResponseWrapper ...[]byte) (Action, error) { ctx := context.Background() aiStarttime := time.Now().UnixMilli() @@ -8576,6 +8742,13 @@ data_filter: } ]`, enableQuestionsString) + // If a template is set, get secondary system rules + extra context + templateSystemRule := "" + templateContext := "" + if len(template) > 0 { + templateSystemRule, templateContext, _ = getTemplateContext(ctx, template, execution) + } + agentReasoningEffort := "low" newReasoningEffort := os.Getenv("AI_AGENT_REASONING_EFFORT") if len(newReasoningEffort) > 0 { @@ -8624,25 +8797,29 @@ data_filter: aiModel = newAiModel } - completionRequest := openai.ChatCompletionRequest{ - Model: aiModel, - Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleSystem, - Content: systemMessage, - }, + primaryMessages := []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: systemMessage, }, + } - // Move towards determinism - Temperature: 0, - ReasoningEffort: agentReasoningEffort, + // Inject template-specific rules as a secondary system message + if len(templateSystemRule) > 0 { + primaryMessages = append(primaryMessages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleSystem, + Content: templateSystemRule, + }) + } - // Reasoning control - // MaxCompletionTokens: 5000, - // ReasoningEffort: agentReasoningEffort, - // Store: true, + completionRequest := openai.ChatCompletionRequest{ + Model: aiModel, + Messages: primaryMessages, + Temperature: 0, + ReasoningEffort: agentReasoningEffort, } + // Inject template-specific context (e.g. workflow state, app actions) into user context preparedContent := fmt.Sprintf("USER CONTEXT:\n%s\n", metadata) if len(imagesIncluded) == 0 { completionRequest.Messages = append(completionRequest.Messages, openai.ChatCompletionMessage{ diff --git a/db-connector.go b/db-connector.go index 88aa5ec9..4a750403 100755 --- a/db-connector.go +++ b/db-connector.go @@ -2568,7 +2568,7 @@ func Fixexecution(ctx context.Context, workflowExecution WorkflowExecution) (Wor time.Sleep(1 * time.Second) sendAgentActionSelfRequest("WAITING", capturedExec, capturedExec.Results[resultIndex]) time.Sleep(2 * time.Second) - _, err := HandleAiAgentExecutionStart(capturedExec, capturedAction, true, "fixexecution_timeout_recovery") + _, err := HandleAiAgentExecutionStart(capturedExec, capturedAction, true, "fixexecution_timeout_recovery", "") if err != nil { log.Printf("[ERROR][%s] Failed re-invoking agent after decisions completed for action %s: %s", capturedExec.ExecutionId, capturedAction.ID, err) } From aadfda6f0c92553a1f989e25614c103c3553d891 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Tue, 21 Jul 2026 22:26:41 +0530 Subject: [PATCH 09/20] update rawField struct --- structs.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/structs.go b/structs.go index 3f042860..53f2ae0a 100755 --- a/structs.go +++ b/structs.go @@ -4902,6 +4902,31 @@ type MinimalParameter struct { Value string `json:"value"` } +func (m *MinimalParameter) UnmarshalJSON(data []byte) error { + type Alias MinimalParameter + var aux struct { + Value json.RawMessage `json:"value"` + *Alias + } + aux.Alias = (*Alias)(m) + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if len(aux.Value) > 0 { + if aux.Value[0] == '"' { + var str string + if err := json.Unmarshal(aux.Value, &str); err != nil { + return err + } + m.Value = str + } else { + m.Value = string(aux.Value) + } + } + return nil +} + // MinimalAction - action with position and basic info type MinimalAction struct { AppName string `json:"app_name"` @@ -5782,7 +5807,8 @@ type WorkflowSetOpsResponse struct { } type rawField struct { - Name string `json:"name"` + Name string `json:"name,omitempty"` + Key string `json:"key,omitempty"` Value interface{} `json:"value"` } From a13141dc2a7c786a63873448d7a542c880161c5a Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Tue, 21 Jul 2026 22:31:20 +0530 Subject: [PATCH 10/20] feat: update workflow ID messaging and enhance system rules for agent context --- ai.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/ai.go b/ai.go index 5b25f72f..dbce4982 100644 --- a/ai.go +++ b/ai.go @@ -7682,22 +7682,21 @@ func buildWorkflowEditContext(ctx context.Context, execution WorkflowExecution) // What we tell the agent about its workflow_id workflowIdLine := "You do NOT have an existing workflow yet. Your FIRST action MUST be to call create_workflow to create one, then use the returned workflow_id for all subsequent operations." if len(targetWorkflowId) > 0 { - workflowIdLine = fmt.Sprintf(`The workflow you are currently working on has the ID: %s — always use this as the "workflow_id" in every payload you send.`, targetWorkflowId) + workflowIdLine = fmt.Sprintf(`Working Workflow ID: %s — use this as the "workflow_id" field in agent_update.`, targetWorkflowId) } - systemRule := fmt.Sprintf(`[SECONDARY]: You are an autonomous workflow-building agent. You build or edit workflows that will eventually automate the task. Only use the special actions built for you — agent_update is the primary one you will use. If the user requests an integration that is not in your current context, you can still fetch its actions by passing the requested app name to your get_workflow_app_actions tool. The system will automatically search for the app and return its actions. - -%s + systemRule := `[SECONDARY]: You are an autonomous workflow-building agent. You build or edit workflows that will eventually automate the task. Only use the special actions built for you — agent_update is the primary one you will use. If the user requests an integration that is not in your current context, you can still fetch its actions by passing the requested app name to your get_workflow_app_actions tool. The system will automatically search for the app and return its actions. Only ask the user for information when it is genuinely unavailable through any allowed action. Use get_minimal_action to fetch the latest workflow state whenever you need to inspect or verify what is currently in the workflow — prefer this over relying on stale context. Payload Structure Send your incremental steps in the operations array. You can send one operation at a time, or multiple if you are building a connected sequence. -{ +Key is "body" and Value is { "workflow_id": "", "operations": [ ...array of step-by-step operations... ] } +CRITICAL: The "operations" field MUST be a JSON array of objects. NEVER write a natural language plan, summary, or string inside the "operations" field. Supported Step-by-Step Operations @@ -7748,6 +7747,7 @@ Format: Schedule Trigger "y": 100 } } +CRITICAL NOTE: The "value" field inside "parameters" MUST ALWAYS be a string. If the parameter expects a JSON object or array, you must escape it into a JSON string (e.g., "value": "{\"key\": \"val\"}"). Do NOT pass raw JSON objects or arrays as values. Note: You can vertically position the node between existing ones by adding "insert_before": "" or "insert_after": "" alongside the data field. 2. CONNECTING NODES (Adding a Branch) @@ -7797,19 +7797,44 @@ CRITICAL RULES FOR THE AGENT 1. Handling Errors: If you make a mistake, the API will reject your request and tell you EXACTLY which operation failed and why (e.g., "Operation 1 failed: app_id is required"). Read the error carefully, fix the specific missing or incorrect field, and try again. 2. Special actions are built exclusively for you. agent_update is the core action that does most of the heavy lifting. -3. Use get_minimal_action to fetch the current workflow state whenever you need to verify what is there — do not assume the context is fresh. -4. Try not to use parallel decision calling; always do sequential tool calls.`, workflowIdLine) +3. Use get_minimal_action to fetch the current workflow state only whenever you need it. +4. Try not to use parallel decision calling; always do sequential tool calls.` templateContext := fmt.Sprintf(`%s +%s %s -`, workflowStateSection, string(appsJson)) +`, workflowIdLine, workflowStateSection, string(appsJson)) return systemRule, templateContext, nil } +func getWorkflowEditPromptRemovals() []string { + return []string{ + ` - **Destructive Guard:** + - If action is DESTRUCTIVE (stop/delete/remove) -> Set "approval_required": true on the action/tool.`, + } +} + +func filterSystemPromptByTemplate(template string, systemMessage string) string { + var removals []string + + switch template { + case "workflow-edit": + removals = getWorkflowEditPromptRemovals() + } + + for _, removal := range removals { + if removal != "" { + systemMessage = strings.ReplaceAll(systemMessage, removal, "") + } + } + + return systemMessage +} + func HandleAiAgentExecutionStart(execution WorkflowExecution, startNode Action, createNextActions bool, callerName string, template string, aiResponseWrapper ...[]byte) (Action, error) { ctx := context.Background() aiStarttime := time.Now().UnixMilli() @@ -8742,11 +8767,17 @@ data_filter: } ]`, enableQuestionsString) - // If a template is set, get secondary system rules + extra context + systemMessage = filterSystemPromptByTemplate(template, systemMessage) + + // If a template is set, get secondary system rules + extra context. + // Only fetch on first run, subsequent steps already have it in HISTORY so no need to re-hit DB. templateSystemRule := "" templateContext := "" - if len(template) > 0 { + if len(template) > 0 && len(marshalledDecisions) <= 4 { templateSystemRule, templateContext, _ = getTemplateContext(ctx, template, execution) + } else if len(template) > 0 { + // Still inject the static system rule on every step, just skip the DB fetch for context + templateSystemRule, _, _ = getTemplateContext(ctx, template, execution) } agentReasoningEffort := "low" @@ -8787,7 +8818,7 @@ data_filter: } // Set model based on environment - aiModel := "gpt-5-mini" + aiModel := "gpt-5.4-2026-03-05" newAiModel := os.Getenv("AI_MODEL") if newAiModel == "" { newAiModel = os.Getenv("OPENAI_MODEL") @@ -8819,8 +8850,16 @@ data_filter: ReasoningEffort: agentReasoningEffort, } - // Inject template-specific context (e.g. workflow state, app actions) into user context + // Inject template-specific context as a completely separate user message + if len(templateContext) > 0 { + completionRequest.Messages = append(completionRequest.Messages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: "TEMPLATE CONTEXT:\n" + templateContext, + }) + } + preparedContent := fmt.Sprintf("USER CONTEXT:\n%s\n", metadata) + if len(imagesIncluded) == 0 { completionRequest.Messages = append(completionRequest.Messages, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleUser, From ddd10806399eb92a6298a54460b157e05df976b3 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Thu, 23 Jul 2026 01:14:51 +0530 Subject: [PATCH 11/20] updated prompt for workflow building --- ai.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/ai.go b/ai.go index dbce4982..9d124efc 100644 --- a/ai.go +++ b/ai.go @@ -7747,16 +7747,26 @@ Format: Schedule Trigger "y": 100 } } -CRITICAL NOTE: The "value" field inside "parameters" MUST ALWAYS be a string. If the parameter expects a JSON object or array, you must escape it into a JSON string (e.g., "value": "{\"key\": \"val\"}"). Do NOT pass raw JSON objects or arrays as values. + +Every action’s response is stored under its label. You can reference it using: +$label_name this itself gives you the parsed JSON output of the action +* $jira_action_1.id +* $python_2.message.email +* For triggers use "$exec" for example $exec.field + Note: You can vertically position the node between existing ones by adding "insert_before": "" or "insert_after": "" alongside the data field. 2. CONNECTING NODES (Adding a Branch) -Connect your nodes using their real IDs (if they already exist) or the temp_id you assigned when creating them in the same payload. +Connect your nodes using their real IDs (if they already exist) or the temp_id you assigned when creating them in the same payload. You can include an array of conditions if you are adding multiple to the same branch. { "op": "add_branch", +"temp_id": "branch_1", "data": { "source_id": "", - "destination_id": "" + "destination_id": "", + "conditions": [ + { "source": "$some_var", "condition": "equals", "destination": "true" } + ] } } @@ -7778,14 +7788,51 @@ Only provide the fields you actually want to change. { "op": "delete_branch", "id": "" } { "op": "move_node", "id": "", "data": { "x": 250, "y": 300 } } -5. SETTING THE START NODE +5. MANAGING CONDITIONS +You can add, edit, or delete conditions on an EXISTING branch. +NOTE: To edit or delete, you must use a Get/Read action first to find the exact condition ID. + +Available operators: equals, larger than, less than, does not equal, startswith, endswith, is empty, contains, contains_any_of. + +Add conditions to an existing branch: +{ +"op": "add_condition", +"branch_id": "", +"data": { + "conditions": [ + { "source": "$var", "condition": "equals", "destination": "val" } + ] +} +} + +Edit existing conditions (only include fields you want to change): +{ +"op": "edit_condition", +"branch_id": "", +"data": { + "conditions": [ + { "id": "", "source": "$new_var", "condition": "contains", "destination": "new_val" } + ] +} +} + +Delete conditions: +{ +"op": "delete_condition", +"branch_id": "", +"data": { + "condition_ids": [""] +} +} + +6. SETTING THE START NODE Defines the entry point of the workflow. You can use a real ID or a temp_id from the same payload. { "op": "set_start_node", "id": "" } -6. SAVING THE WORKFLOW +7. SAVING THE WORKFLOW Your edits are stored as a real-time draft. When you are completely finished building or modifying the workflow, you MUST append this operation to your final payload to permanently save the workflow to the database. Call this only after all changes are complete — do not forget it. { "op": "save_workflow" @@ -7815,6 +7862,11 @@ func getWorkflowEditPromptRemovals() []string { return []string{ ` - **Destructive Guard:** - If action is DESTRUCTIVE (stop/delete/remove) -> Set "approval_required": true on the action/tool.`, + `// true IF the action seems risky or destructive and requires user approval. Otherwise false`, + `### DATA REDUCTION: +data_filter: +- "full": The default value of the data_filter is full. Use for all non-data-returning calls or when you need the entire response. +- "list": Use for ALL data calls. Request ONLY essential fields. If the schema is completely unknown, fallback to "full"`, } } From bb4d4a797c3ffe125ee0140cd0703137abc82aa8 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Thu, 23 Jul 2026 01:16:38 +0530 Subject: [PATCH 12/20] unify condition ID generation and enhance condition operations handling --- shared.go | 127 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/shared.go b/shared.go index a8a8c978..b8feb701 100644 --- a/shared.go +++ b/shared.go @@ -38296,20 +38296,21 @@ func generateNodeID() string { } func createCondition(sourceVal, conditionVal, destVal string) Condition { + sharedID := generateNodeID() return Condition{ Source: WorkflowAppActionParameter{ - ID: generateNodeID(), + ID: sharedID, Name: "source", Variant: "STATIC_VALUE", Value: sourceVal, }, Condition: WorkflowAppActionParameter{ - ID: generateNodeID(), + ID: sharedID, Name: "condition", Value: conditionVal, }, Destination: WorkflowAppActionParameter{ - ID: generateNodeID(), + ID: sharedID, Name: "destination", Variant: "STATIC_VALUE", Value: destVal, @@ -38887,6 +38888,23 @@ func collectStreamOps(wf *Workflow, op *WorkflowOperation, tempIDMap map[string] ID: realID, }} + case "add_condition", "edit_condition", "delete_condition": + // The branch already has its updated conditions applied. + // Stream edge:configure with the full branch so the frontend re-renders it. + branchID := op.BranchID + for _, branch := range wf.Branches { + if branch.ID == branchID { + dataBytes, _ := json.Marshal(branch) + return []StreamWorkflowOperation{{ + Item: "edge", + Type: "configure", + ID: branch.ID, + Data: dataBytes, + }} + } + } + return nil + default: return nil } @@ -38951,12 +38969,20 @@ func applyWorkflowOperationWithMapping(ctx context.Context, user User, wf *Workf return opDeleteBranch(wf, op) // ====== CONDITION OPERATIONS ====== - case "add_condition": - return opAddCondition(wf, op) - case "edit_condition": - return opEditCondition(wf, op) - case "delete_condition": - return opDeleteCondition(wf, op) + case "add_condition", "edit_condition", "delete_condition": + if realBranchID, exists := tempIDMap[op.BranchID]; exists { + op.BranchID = realBranchID + } + + switch op.Op { + case "add_condition": + return opAddCondition(wf, op) + case "edit_condition": + return opEditCondition(wf, op) + case "delete_condition": + return opDeleteCondition(wf, op) + } + return nil // ====== WORKFLOW OPERATIONS ====== case "set_start_node": @@ -39366,6 +39392,11 @@ func opAddBranch(wf *Workflow, op *WorkflowOperation) error { SourceID string `json:"source_id"` DestinationID string `json:"destination_id"` Label string `json:"label"` + Conditions []struct { + Source string `json:"source"` + Condition string `json:"condition"` + Destination string `json:"destination"` + } `json:"conditions"` } if err := json.Unmarshal(op.Data, &branchData); err != nil { @@ -39391,6 +39422,10 @@ func opAddBranch(wf *Workflow, op *WorkflowOperation) error { Conditions: []Condition{}, } + for _, c := range branchData.Conditions { + newBranch.Conditions = append(newBranch.Conditions, createCondition(c.Source, c.Condition, c.Destination)) + } + // Detect circular references before adding branch if hasCircularBranch(wf, newBranch) { return fmt.Errorf("circular branch detected: would create loop from %s → %s", branchData.SourceID, branchData.DestinationID) @@ -39440,9 +39475,11 @@ func opDeleteBranch(wf *Workflow, op *WorkflowOperation) error { func opAddCondition(wf *Workflow, op *WorkflowOperation) error { var condData struct { - Source string `json:"source"` - Condition string `json:"condition"` - Destination string `json:"destination"` + Conditions []struct { + Source string `json:"source"` + Condition string `json:"condition"` + Destination string `json:"destination"` + } `json:"conditions"` } if err := json.Unmarshal(op.Data, &condData); err != nil { @@ -39454,8 +39491,10 @@ func opAddCondition(wf *Workflow, op *WorkflowOperation) error { return fmt.Errorf("branch %s not found", op.BranchID) } - newCond := createCondition(condData.Source, condData.Condition, condData.Destination) - wf.Branches[branchIdx].Conditions = append(wf.Branches[branchIdx].Conditions, newCond) + for _, c := range condData.Conditions { + newCond := createCondition(c.Source, c.Condition, c.Destination) + wf.Branches[branchIdx].Conditions = append(wf.Branches[branchIdx].Conditions, newCond) + } return nil } @@ -39466,25 +39505,35 @@ func opEditCondition(wf *Workflow, op *WorkflowOperation) error { return fmt.Errorf("branch %s not found", op.BranchID) } - if op.ConditionIndex < 0 || op.ConditionIndex >= len(wf.Branches[branchIdx].Conditions) { - return fmt.Errorf("condition index %d out of range", op.ConditionIndex) - } - var condData struct { - Source string `json:"source"` - Condition string `json:"condition"` - Destination string `json:"destination"` + Conditions []struct { + ID string `json:"id"` + Source *string `json:"source"` + Condition *string `json:"condition"` + Destination *string `json:"destination"` + } `json:"conditions"` } if err := json.Unmarshal(op.Data, &condData); err != nil { return fmt.Errorf("invalid condition update data: %w", err) } - wf.Branches[branchIdx].Conditions[op.ConditionIndex] = createCondition( - condData.Source, - condData.Condition, - condData.Destination, - ) + for _, update := range condData.Conditions { + for cIdx := range wf.Branches[branchIdx].Conditions { + if wf.Branches[branchIdx].Conditions[cIdx].Condition.ID == update.ID { + if update.Source != nil { + wf.Branches[branchIdx].Conditions[cIdx].Source.Value = *update.Source + } + if update.Condition != nil { + wf.Branches[branchIdx].Conditions[cIdx].Condition.Value = *update.Condition + } + if update.Destination != nil { + wf.Branches[branchIdx].Conditions[cIdx].Destination.Value = *update.Destination + } + break + } + } + } return nil } @@ -39495,14 +39544,28 @@ func opDeleteCondition(wf *Workflow, op *WorkflowOperation) error { return fmt.Errorf("branch %s not found", op.BranchID) } - if op.ConditionIndex < 0 || op.ConditionIndex >= len(wf.Branches[branchIdx].Conditions) { - return fmt.Errorf("condition index %d out of range", op.ConditionIndex) + var deleteData struct { + ConditionIDs []string `json:"condition_ids"` } - wf.Branches[branchIdx].Conditions = append( - wf.Branches[branchIdx].Conditions[:op.ConditionIndex], - wf.Branches[branchIdx].Conditions[op.ConditionIndex+1:]..., - ) + if err := json.Unmarshal(op.Data, &deleteData); err != nil { + return fmt.Errorf("invalid delete condition data: %w", err) + } + + var newConds []Condition + for _, c := range wf.Branches[branchIdx].Conditions { + deleted := false + for _, delID := range deleteData.ConditionIDs { + if c.Condition.ID == delID { + deleted = true + break + } + } + if !deleted { + newConds = append(newConds, c) + } + } + wf.Branches[branchIdx].Conditions = newConds return nil } From b249bca7a6e60fd90623bde4cfda93ee31da0ecb Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Thu, 23 Jul 2026 15:29:22 +0530 Subject: [PATCH 13/20] added new app for auth skiip --- blobs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blobs.go b/blobs.go index 95242824..f481da96 100644 --- a/blobs.go +++ b/blobs.go @@ -22,8 +22,8 @@ import ( func IsShuffleApp(app WorkflowApp) bool { parsedAppname := strings.ReplaceAll(strings.ToLower(app.Name), " ", "_") - skipAuthAppnames := []string{"openai", "shuffle_datastore", "shuffle_workflows", "shuffle_detection", "shuffle_sensors", "shuffle_monitors", "shuffle_host_monitors", "shuffle_apps"} - skipAuthAppIds := []string{"5d19dd82517870c68d40cacad9b5ca91", "b82668d868f6dc7ac1dc14caa92c674b", "b598b078fd5c531699fca803c172ce72", "afda48b8d1f7dc7ac3caae87b2c072e9", "7f12d725c356677d28db042170444448", "48a954b9440b3913b8a2620e57b94a75", "7db43ccd25261967b095cfbd467a75cc"} + skipAuthAppnames := []string{"openai", "shuffle_datastore", "shuffle_workflows", "shuffle_detection", "shuffle_sensors", "shuffle_monitors", "shuffle_host_monitors", "shuffle_apps", "shuffle_workflows_builder"} + skipAuthAppIds := []string{"5d19dd82517870c68d40cacad9b5ca91", "b82668d868f6dc7ac1dc14caa92c674b", "b598b078fd5c531699fca803c172ce72", "afda48b8d1f7dc7ac3caae87b2c072e9", "7f12d725c356677d28db042170444448", "48a954b9440b3913b8a2620e57b94a75", "7db43ccd25261967b095cfbd467a75cc", "de4ef2287bd41b9d5563e39989643ee6"} isShuffleApp := false if project.Environment == "cloud" && len(app.ID) > 0 { From bd0fc9df2d2cb8e3e727379c4229c1bee8a6d4d9 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Thu, 23 Jul 2026 15:31:22 +0530 Subject: [PATCH 14/20] feat: enhance app retrieval logic with global fallback and auto-activation --- shared.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/shared.go b/shared.go index b8feb701..1500e575 100644 --- a/shared.go +++ b/shared.go @@ -37964,12 +37964,35 @@ func GetWorkflowAppActions(resp http.ResponseWriter, request *http.Request) { // Search by IDs first (more direct) if len(actionReq.AppIDs) > 0 { for _, appID := range actionReq.AppIDs { + foundInOrg := false for _, app := range allApps { if app.ID == appID && len(app.Name) > 0 { + if debug { + log.Printf("[DEBUG] Found app by ID %s in org apps list", appID) + } matchedApps = append(matchedApps, app) + foundInOrg = true break } } + + // If not found in org's active apps, fetch it globally by ID! + if !foundInOrg { + if debug { + log.Printf("[DEBUG] App ID %s not in org, trying global GetApp", appID) + } + globalApp, err := GetApp(ctx, appID, user, false) + if err == nil && globalApp != nil { + if debug { + log.Printf("[DEBUG] Successfully found app %s globally", appID) + } + matchedApps = append(matchedApps, *globalApp) + } else { + if debug { + log.Printf("[DEBUG] Failed to fetch app globally by ID %s: %v", appID, err) + } + } + } } } @@ -37980,6 +38003,9 @@ func GetWorkflowAppActions(resp http.ResponseWriter, request *http.Request) { foundInOrg := false for _, app := range allApps { if strings.ToLower(app.Name) == lowerName && len(app.ID) > 0 { + if debug { + log.Printf("[DEBUG] Found app by name '%s' (ID %s) in org apps list", appName, app.ID) + } // Check if already added via ID search alreadyAdded := false for _, matched := range matchedApps { @@ -37998,8 +38024,14 @@ func GetWorkflowAppActions(resp http.ResponseWriter, request *http.Request) { // If not found in org, search Algolia if !foundInOrg { + if debug { + log.Printf("[DEBUG] App Name '%s' not in org, searching Algolia", appName) + } algoliaApp, err := HandleAlgoliaAppSearch(ctx, appName) if err == nil && algoliaApp.ObjectID != "" { + if debug { + log.Printf("[DEBUG] Found Algolia match for '%s': ObjectID %s", appName, algoliaApp.ObjectID) + } discoveredApp, err := GetApp(ctx, algoliaApp.ObjectID, user, false) if err == nil && discoveredApp != nil { alreadyAdded := false @@ -38010,6 +38042,9 @@ func GetWorkflowAppActions(resp http.ResponseWriter, request *http.Request) { } } if !alreadyAdded { + if debug { + log.Printf("[DEBUG] Appended Algolia app %s to matched apps", discoveredApp.ID) + } matchedApps = append(matchedApps, *discoveredApp) } } @@ -38202,7 +38237,7 @@ func AgentWorkflowEditor(resp http.ResponseWriter, request *http.Request) { // All context building (workflow state, app actions, rules) is handled inside // buildWorkflowEditContext which is called by getTemplateContext inside HandleAiAgentExecutionStart - toolApps := "app:7db43ccd25261967b095cfbd467a75cc:shuffle_apps,app:b598b078fd5c531699fca803c172ce72:shuffle_workflows" + toolApps := "app:7db43ccd25261967b095cfbd467a75cc:shuffle_apps,app:de4ef2287bd41b9d5563e39989643ee6:shuffle_workflows_builder" action := Action{ ID: uuid.NewV4().String(), @@ -38329,8 +38364,55 @@ func findAppByID(ctx context.Context, appID string, user User) (*WorkflowApp, er return app, err } + // 1. Search Org's Prioritized Apps first + prioritizedApps, err := GetPrioritizedApps(ctx, user) + if err == nil { + for _, pApp := range prioritizedApps { + if pApp.ID == appID { + return &pApp, nil + } + } + } + + // 2. If not found in org apps, try getting it globally app, err := GetApp(ctx, appID, user, false) - return app, err + if err != nil { + return nil, err + } + + // 3. If we got it globally but it wasn't in prioritized apps, auto-activate it in parallel + go func(activateAppID string, apiKey string) { + baseURL := os.Getenv("BASE_URL") + if len(baseURL) == 0 { + if len(os.Getenv("SHUFFLE_CLOUDRUN_URL")) > 0 { + baseURL = os.Getenv("SHUFFLE_CLOUDRUN_URL") + } else { + port := os.Getenv("PORT") + if len(port) == 0 { + port = "5001" + } + baseURL = fmt.Sprintf("http://localhost:%s", port) + } + } + + activateURL := fmt.Sprintf("%s/api/v1/apps/%s/activate", baseURL, activateAppID) + req, err := http.NewRequest("GET", activateURL, nil) + if err != nil { + log.Printf("[WARNING] Failed building auto-activate request for app %s: %s", activateAppID, err) + return + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey)) + httpResp, err := http.DefaultClient.Do(req) + if err != nil { + log.Printf("[WARNING] Failed auto-activating app %s: %s", activateAppID, err) + return + } + httpResp.Body.Close() + //log.Printf("[INFO] Auto-activated app %s in the background", activateAppID) + }(appID, user.ApiKey) + + return app, nil } func enrichActionFromApp(ctx context.Context, minAct *MinimalAction, realApp *WorkflowApp, environment string) (Action, error) { From d7ee7873bc2bf0e9cd8a6a41f53fe1e86cb46619 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Thu, 23 Jul 2026 23:52:35 +0530 Subject: [PATCH 15/20] removed the template param from Agent starter function and added auto start and delete of triggers --- ai.go | 36 ++++++++++--- shared.go | 159 +++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 170 insertions(+), 25 deletions(-) diff --git a/ai.go b/ai.go index 9d124efc..a7c2af14 100644 --- a/ai.go +++ b/ai.go @@ -7696,7 +7696,28 @@ Key is "body" and Value is { "workflow_id": "", "operations": [ ...array of step-by-step operations... ] } -CRITICAL: The "operations" field MUST be a JSON array of objects. NEVER write a natural language plan, summary, or string inside the "operations" field. + +CRITICAL: The "operations" field MUST be a JSON array of objects. NEVER write a natural language plan, summary, or string inside the "operations" field. This overrides the generic "value": "literal_value" example shown elsewhere in your instructions — for agent_update, "value" is ALWAYS an object, and "operations" inside it is ALWAYS an array, never a string. + +Concrete example of a correct full decision using agent_update: +{ + "action": "agent_update", + "category": "singul", + "tool": "agent_update", + "fields": [ + { + "key": "body", + "value": { + "workflow_id": "", + "operations": [ + { "op": "edit_node", "id": "", "data": { "parameters": [ { "name": "method", "value": "GET" }, { "name": "url", "value": "https://example.com" } ] } } + ] + } + } + ] +} + +INCORRECT (do NOT do this): { "key": "body", "value": { "operations": "Update the workflow actions as follows: ...", "workflow_id": "" } } Supported Step-by-Step Operations @@ -7887,7 +7908,8 @@ func filterSystemPromptByTemplate(template string, systemMessage string) string return systemMessage } -func HandleAiAgentExecutionStart(execution WorkflowExecution, startNode Action, createNextActions bool, callerName string, template string, aiResponseWrapper ...[]byte) (Action, error) { +func HandleAiAgentExecutionStart(execution WorkflowExecution, startNode Action, createNextActions bool, callerName string, aiResponseWrapper ...[]byte) (Action, error) { + template := "" ctx := context.Background() aiStarttime := time.Now().UnixMilli() @@ -8080,6 +8102,10 @@ func HandleAiAgentExecutionStart(execution WorkflowExecution, startNode Action, imagesIncluded := []string{} imageDetail := openai.ImageURLDetailAuto // low, high, original, auto (let the model decide) for _, param := range startNode.Parameters { + if param.Name == "template" && len(template) == 0 { + template = param.Value + } + if param.Name == "input" { userMessage = param.Value @@ -8822,14 +8848,10 @@ data_filter: systemMessage = filterSystemPromptByTemplate(template, systemMessage) // If a template is set, get secondary system rules + extra context. - // Only fetch on first run, subsequent steps already have it in HISTORY so no need to re-hit DB. templateSystemRule := "" templateContext := "" - if len(template) > 0 && len(marshalledDecisions) <= 4 { + if len(template) > 0 { templateSystemRule, templateContext, _ = getTemplateContext(ctx, template, execution) - } else if len(template) > 0 { - // Still inject the static system rule on every step, just skip the DB fetch for context - templateSystemRule, _, _ = getTemplateContext(ctx, template, execution) } agentReasoningEffort := "low" diff --git a/shared.go b/shared.go index 1500e575..9e6a9727 100644 --- a/shared.go +++ b/shared.go @@ -18194,7 +18194,7 @@ func handleAgentDecisionStreamResult(workflowExecution WorkflowExecution, action } callerName := "handleAgentDecisionStreamResult" - returnAction, err := HandleAiAgentExecutionStart(workflowExecution, originalAction, true, callerName, "") + returnAction, err := HandleAiAgentExecutionStart(workflowExecution, originalAction, true, callerName) if err != nil { log.Printf("[ERROR][%s] Failed handling agent execution start: %s", workflowExecution.ExecutionId, err) } @@ -18307,7 +18307,7 @@ func ParsedExecutionResult(ctx context.Context, workflowExecution WorkflowExecut if err != nil { log.Printf("[ERROR] AI Agent (10): Failed marshalling actionResult: %s", err) } else { - go HandleAiAgentExecutionStart(*foundParentExec, startNode, false, callerName, "", marshalledResult) + go HandleAiAgentExecutionStart(*foundParentExec, startNode, false, callerName, marshalledResult) } } } else { @@ -22503,7 +22503,7 @@ func PrepareSingleAction(ctx context.Context, parentRequest *http.Request, user log.Printf("[INFO][%s] AI Agent: %s Started standalone for org %s, execution id %s, workflow %s", exec.ExecutionId, callerName, user.ActiveOrg.Id, exec.ExecutionId, exec.WorkflowId) - action, err := HandleAiAgentExecutionStart(exec, action, false, callerName, "") + action, err := HandleAiAgentExecutionStart(exec, action, false, callerName) if err != nil { log.Printf("[ERROR] Failed to handle AI agent execution start: %s", err) } @@ -38255,6 +38255,10 @@ func AgentWorkflowEditor(resp http.ResponseWriter, request *http.Request) { Name: "action", Value: toolApps, }, + { + Name: "template", + Value: "workflow-edit", + }, }, } @@ -38288,7 +38292,7 @@ func AgentWorkflowEditor(resp http.ResponseWriter, request *http.Request) { log.Printf("[INFO] AgentWorkflowEditor: calling HandleAiAgentExecutionStart for user %s (%s), target_workflow_id=%s, execution_id=%s", user.Username, user.Id, req.Params.Input.WorkflowId, exec.ExecutionId) - returnAction, err := HandleAiAgentExecutionStart(exec, action, false, "AgentWorkflowEditor", "workflow-edit") + returnAction, err := HandleAiAgentExecutionStart(exec, action, false, "AgentWorkflowEditor") if err != nil { log.Printf("[ERROR] HandleAiAgentExecutionStart failed in AgentWorkflowEditor: %s", err) resp.WriteHeader(500) @@ -38665,6 +38669,10 @@ func HandleAgentWorkflowOperations(resp http.ResponseWriter, request *http.Reque tempIDMap := make(map[string]string) // Maps temp_id → real_id shouldSaveDB := false + // Track triggers to start/stop AFTER all operations are applied. + var addedTriggers []Trigger + var deletedTriggers []Trigger + // Collect stream ops as we apply each operation. var streamOps []StreamWorkflowOperation @@ -38679,10 +38687,23 @@ func HandleAgentWorkflowOperations(resp http.ResponseWriter, request *http.Reque // For delete_node: capture which branch IDs are connected to this node BEFORE apply, because opDeleteNode silently prunes them from wf.Branches. We'll stream an edge:remove for each one auto-cleaned. var prunedBranchIDs []string - if operation.Op == "delete_node" { - for _, br := range workflow.Branches { - if br.SourceID == operation.ID || br.DestinationID == operation.ID { - prunedBranchIDs = append(prunedBranchIDs, br.ID) + if operation.Op == "delete_node" || operation.Op == "remove_node" { + for _, branch := range workflow.Branches { + if branch.SourceID == operation.ID || branch.DestinationID == operation.ID { + prunedBranchIDs = append(prunedBranchIDs, branch.ID) + } + } + } + + // Track triggers being added/deleted so we can start/stop them AFTER + // the full workflow is constructed (start node + branches all resolved). + var triggerToStop *Trigger + if (operation.Op == "delete_node" || operation.Op == "remove_node") && operation.NodeType == "trigger" { + for _, existingTrigger := range workflow.Triggers { + if existingTrigger.ID == operation.ID { + copy := existingTrigger + triggerToStop = © + break } } } @@ -38696,6 +38717,17 @@ func HandleAgentWorkflowOperations(resp http.ResponseWriter, request *http.Reque return } streamOps = append(streamOps, collectStreamOps(workflow, &operation, tempIDMap, branchCountBefore, prunedBranchIDs)...) + + // After apply: track triggers that were just added + if operation.Op == "add_node" && operation.NodeType == "trigger" { + // The newly added trigger is always the last in the Triggers slice + if len(workflow.Triggers) > 0 { + addedTriggers = append(addedTriggers, workflow.Triggers[len(workflow.Triggers)-1]) + } + } + if triggerToStop != nil { + deletedTriggers = append(deletedTriggers, *triggerToStop) + } } // Save to cache (volatile, 30 min TTL) @@ -38713,6 +38745,83 @@ func HandleAgentWorkflowOperations(resp http.ResponseWriter, request *http.Reque // Don't fail the request, cache is best-effort } + // Fire trigger start/stop goroutines NOW — workflow is fully constructed + // with all nodes, branches and start node resolved. + if len(addedTriggers) > 0 || len(deletedTriggers) > 0 { + go func(triggersToStart []Trigger, triggersToStop []Trigger, finalWorkflow Workflow, currentUser User) { + for _, trigger := range triggersToStart { + // Resolve start node: prefer a branch from this trigger, else use workflow start. + startNode := finalWorkflow.Start + for _, branch := range finalWorkflow.Branches { + if branch.SourceID == trigger.ID { + startNode = branch.DestinationID + break + } + } + if len(startNode) == 0 { + log.Printf("[INFO] Auto-start skipped for trigger %s: no start node in fully-constructed workflow", trigger.ID) + continue + } + + switch strings.ToUpper(trigger.TriggerType) { + case "WEBHOOK": + auth := "" + customResponse := "" + version := "v1" + versionTimeout := 15 + for _, param := range trigger.Parameters { + switch param.Name { + case "auth_headers": + auth = param.Value + case "custom_response_body": + customResponse = param.Value + case "await_response": + version = param.Value + case "version_timeout": + if parsedTimeout, convErr := strconv.Atoi(param.Value); convErr == nil { + versionTimeout = parsedTimeout + } + } + } + if startErr := startWebhookTrigger(context.Background(), finalWorkflow.ID, trigger.ID, trigger.Label, startNode, trigger.Environment, auth, customResponse, version, versionTimeout, currentUser, currentUser.ActiveOrg.Id); startErr != nil { + log.Printf("[WARNING] Auto-start webhook trigger %s failed: %s", trigger.ID, startErr) + } else { + log.Printf("[INFO] Auto-started webhook trigger %s for workflow %s", trigger.ID, finalWorkflow.ID) + } + case "SCHEDULE": + if startErr := startSchedule(trigger, currentUser.ApiKey, finalWorkflow); startErr != nil { + log.Printf("[WARNING] Auto-start schedule trigger %s failed: %s", trigger.ID, startErr) + } else { + log.Printf("[INFO] Auto-started schedule trigger %s for workflow %s", trigger.ID, finalWorkflow.ID) + } + } + } + + for _, trigger := range triggersToStop { + switch strings.ToUpper(trigger.TriggerType) { + case "WEBHOOK": + hook, err := GetHook(context.Background(), trigger.ID) + if err != nil { + log.Printf("[WARNING] Auto-stop webhook: could not find hook %s: %s", trigger.ID, err) + continue + } + hook.Status = "stopped" + hook.Running = false + if setErr := SetHook(context.Background(), *hook); setErr != nil { + log.Printf("[WARNING] Auto-stop webhook: failed updating hook %s status: %s", trigger.ID, setErr) + } + log.Printf("[INFO] Auto-stopped webhook trigger %s for workflow %s", trigger.ID, finalWorkflow.ID) + case "SCHEDULE": + if stopErr := deleteScheduleGeneral(context.Background(), trigger.ID); stopErr != nil { + log.Printf("[WARNING] Auto-stop schedule trigger %s failed: %s", trigger.ID, stopErr) + } else { + log.Printf("[INFO] Auto-stopped schedule trigger %s for workflow %s", trigger.ID, finalWorkflow.ID) + } + } + } + }(addedTriggers, deletedTriggers, *workflow, user) + } + if shouldSaveDB { env := workflow.ExecutionEnvironment if len(env) == 0 { @@ -38759,6 +38868,20 @@ func HandleAgentWorkflowOperations(resp http.ResponseWriter, request *http.Reque } } + // After hydration, update any stream operations with the final state of the actions, + // so the frontend receives the auto-assigned AuthenticationId in real-time. + for s := range streamOps { + if streamOps[s].Item == "node" && (streamOps[s].Type == "add" || streamOps[s].Type == "configure") { + for _, action := range workflow.Actions { + if action.ID == streamOps[s].ID { + dataBytes, _ := json.Marshal(action) + streamOps[s].Data = dataBytes + break + } + } + } + } + err = SetWorkflow(ctx, *workflow, workflow.ID) if err != nil { log.Printf("[ERROR] Failed saving workflow to DB: %s", err) @@ -39039,7 +39162,7 @@ func applyWorkflowOperationWithMapping(ctx context.Context, user User, wf *Workf return opEditNode(wf, op) case "move_node": return opMoveNode(wf, op) - case "delete_node": + case "delete_node", "remove_node": return opDeleteNode(wf, op) // ====== BRANCH OPERATIONS ====== @@ -39047,7 +39170,7 @@ func applyWorkflowOperationWithMapping(ctx context.Context, user User, wf *Workf return opAddBranchWithMapping(wf, op, tempIDMap) case "edit_branch": return opEditBranch(wf, op) - case "delete_branch": + case "delete_branch", "remove_branch": return opDeleteBranch(wf, op) // ====== CONDITION OPERATIONS ====== @@ -39269,6 +39392,7 @@ func opAddNode(ctx context.Context, user User, wf *Workflow, op *WorkflowOperati } else { wf.Triggers = append(wf.Triggers, newTrigger) } + return nil default: @@ -39382,7 +39506,6 @@ func opDeleteNode(wf *Workflow, op *WorkflowOperation) error { if debug { log.Printf("[DEBUG] delete_node(action): action %s not found, already removed - skipping", op.ID) } - return nil } @@ -39391,9 +39514,9 @@ func opDeleteNode(wf *Workflow, op *WorkflowOperation) error { // Remove branches connected to this node var newBranches []Branch - for _, br := range wf.Branches { - if br.SourceID != op.ID && br.DestinationID != op.ID { - newBranches = append(newBranches, br) + for _, branch := range wf.Branches { + if branch.SourceID != op.ID && branch.DestinationID != op.ID { + newBranches = append(newBranches, branch) } } wf.Branches = newBranches @@ -39401,7 +39524,7 @@ func opDeleteNode(wf *Workflow, op *WorkflowOperation) error { case "trigger": idx := findTriggerIndexByID(wf, op.ID) if idx == -1 { - // Already gone idempotent no-op + // Already gone, idempotent no-op if debug { log.Printf("[DEBUG] delete_node(trigger): trigger %s not found, already removed - skipping", op.ID) } @@ -39412,9 +39535,9 @@ func opDeleteNode(wf *Workflow, op *WorkflowOperation) error { // Remove branches connected to this trigger (both source and destination) var newBranches []Branch - for _, br := range wf.Branches { - if br.SourceID != op.ID && br.DestinationID != op.ID { - newBranches = append(newBranches, br) + for _, branch := range wf.Branches { + if branch.SourceID != op.ID && branch.DestinationID != op.ID { + newBranches = append(newBranches, branch) } } wf.Branches = newBranches From f0200f215aae3653446f7ba0a437f20544a89a0e Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Thu, 23 Jul 2026 23:56:44 +0530 Subject: [PATCH 16/20] revert back to the old signature for HandleAiAgentExecutionStart --- db-connector.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-connector.go b/db-connector.go index 4a750403..88aa5ec9 100755 --- a/db-connector.go +++ b/db-connector.go @@ -2568,7 +2568,7 @@ func Fixexecution(ctx context.Context, workflowExecution WorkflowExecution) (Wor time.Sleep(1 * time.Second) sendAgentActionSelfRequest("WAITING", capturedExec, capturedExec.Results[resultIndex]) time.Sleep(2 * time.Second) - _, err := HandleAiAgentExecutionStart(capturedExec, capturedAction, true, "fixexecution_timeout_recovery", "") + _, err := HandleAiAgentExecutionStart(capturedExec, capturedAction, true, "fixexecution_timeout_recovery") if err != nil { log.Printf("[ERROR][%s] Failed re-invoking agent after decisions completed for action %s: %s", capturedExec.ExecutionId, capturedAction.ID, err) } From 0925a0875d458c8601664a5bfec8e38859f73f86 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Thu, 23 Jul 2026 23:58:03 +0530 Subject: [PATCH 17/20] this is temporary fix for singul wrapper issue and needed to be reverted after root fix --- cloudSync.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/cloudSync.go b/cloudSync.go index 9578099b..d4ddc8a9 100755 --- a/cloudSync.go +++ b/cloudSync.go @@ -2231,7 +2231,25 @@ func RunAgentDecisionSingulActionHandler(execution WorkflowExecution, decision A } if outputMapped.Success == false { - return originalBody, debugUrl, appname, []string{}, "", errors.New("Failed running agent decision (3). Success false for Singul action") + // The outer Singul wrapper said success=false, but the actual underlying + // HTTP call may have still worked (this happens with new/custom apps that + // Singul doesn't fully recognise at the schema level). + // By this point `body` already holds the extracted inner raw_response content + // (decoded above). Check if that inner result was actually successful before + // giving up — we only fail hard if the inner call also failed. + var innerResult struct { + Status int `json:"status"` + Success bool `json:"success"` + } + innerUnmarshalErr := json.Unmarshal(body, &innerResult) + + innerCallSucceeded := innerUnmarshalErr == nil && (innerResult.Success || innerResult.Status == 200) + if !innerCallSucceeded { + log.Printf("[ERROR][%s] AI Agent: Singul action failed (success=false) and inner raw_response also shows failure. Returning error.", execution.ExecutionId) + return originalBody, debugUrl, appname, []string{}, "", errors.New("Failed running agent decision (3). Success false for Singul action") + } + + log.Printf("[INFO][%s] AI Agent: Singul outer wrapper returned success=false but inner raw_response shows success (status=%d). Treating as success for custom/new app.", execution.ExecutionId, innerResult.Status) } /* From 28ff74b8d59cbb67100c02c5b8250a074325183f Mon Sep 17 00:00:00 2001 From: monilprajapati Date: Fri, 24 Jul 2026 11:23:00 +0530 Subject: [PATCH 18/20] Returning the minimal workflow based on the query --- shared.go | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/shared.go b/shared.go index 9e6a9727..68c1164a 100644 --- a/shared.go +++ b/shared.go @@ -6842,12 +6842,36 @@ func SetNewWorkflow(resp http.ResponseWriter, request *http.Request) { } } - workflowjson, err := json.Marshal(workflow) - if err != nil { - log.Printf("Failed workflow json setting marshalling: %s", err) - resp.WriteHeader(http.StatusInternalServerError) - resp.Write([]byte(`{"success": false}`)) - return + type minimalWorkflowWithId struct { + *MinimalWorkflow + WorkflowId string `json:"workflow_id"` + } + + var workflowjson []byte + if request.URL.Query().Get("minimal") == "true" { + minimalWorkflow := buildMinimalWorkflow(&workflow) + if minimalWorkflow == nil { + log.Printf("[ERROR] Failed building minimal workflow %s", workflow.ID) + resp.WriteHeader(http.StatusInternalServerError) + resp.Write([]byte(`{"success": false}`)) + return + } + + workflowjson, err = json.Marshal(minimalWorkflowWithId{minimalWorkflow, workflow.ID}) + if err != nil { + log.Printf("Failed minimal workflow json setting marshalling: %s", err) + resp.WriteHeader(http.StatusInternalServerError) + resp.Write([]byte(`{"success": false}`)) + return + } + } else { + workflowjson, err = json.Marshal(workflow) + if err != nil { + log.Printf("Failed workflow json setting marshalling: %s", err) + resp.WriteHeader(http.StatusInternalServerError) + resp.Write([]byte(`{"success": false}`)) + return + } } err = SetWorkflow(ctx, workflow, workflow.ID) From 4984a01eb947a568c30e8a0db52de6ee089f6bf5 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Fri, 24 Jul 2026 23:56:05 +0530 Subject: [PATCH 19/20] refactor: enhance workflow state messaging for clarity and emphasis --- ai.go | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/ai.go b/ai.go index a7c2af14..e39bca21 100644 --- a/ai.go +++ b/ai.go @@ -7666,23 +7666,11 @@ func buildWorkflowEditContext(ctx context.Context, execution WorkflowExecution) } appsJson, _ := json.Marshal(appSummaries) - // Workflow state section, skip fetch if creating from scratch - workflowStateSection := "\n(No existing workflow — you are creating one from scratch. Use create_workflow first, then work on it.)\n" - if len(targetWorkflowId) > 0 { - workflow, wErr := GetWorkflow(ctx, targetWorkflowId) - if wErr != nil { - log.Printf("[WARNING] buildWorkflowEditContext: failed to get workflow %s: %s", targetWorkflowId, wErr) - } else { - minimalWorkflow := buildMinimalWorkflow(workflow) - minimalWorkflowJson, _ := json.Marshal(minimalWorkflow) - workflowStateSection = fmt.Sprintf("\n%s\n", string(minimalWorkflowJson)) - } - } - // What we tell the agent about its workflow_id - workflowIdLine := "You do NOT have an existing workflow yet. Your FIRST action MUST be to call create_workflow to create one, then use the returned workflow_id for all subsequent operations." + workflowIdLine := "CRITICAL: You do NOT have an existing workflow yet. Your FIRST action MUST be to call create_workflow to create one, then ALWAYS use the returned workflow_id for ALL subsequent operations." if len(targetWorkflowId) > 0 { - workflowIdLine = fmt.Sprintf(`Working Workflow ID: %s — use this as the "workflow_id" field in agent_update.`, targetWorkflowId) + workflowIdLine = fmt.Sprintf(`CRITICAL: Working Workflow ID: %s +You MUST use this exact workflow_id in every single agent_update operation. DO NOT try to guess or create a new one.`, targetWorkflowId) } systemRule := `[SECONDARY]: You are an autonomous workflow-building agent. You build or edit workflows that will eventually automate the task. Only use the special actions built for you — agent_update is the primary one you will use. If the user requests an integration that is not in your current context, you can still fetch its actions by passing the requested app name to your get_workflow_app_actions tool. The system will automatically search for the app and return its actions. @@ -7869,11 +7857,10 @@ CRITICAL RULES FOR THE AGENT 4. Try not to use parallel decision calling; always do sequential tool calls.` templateContext := fmt.Sprintf(`%s -%s %s -`, workflowIdLine, workflowStateSection, string(appsJson)) +`, workflowIdLine, string(appsJson)) return systemRule, templateContext, nil } From 83fd489bca60fcc290e7ec7c71304329236a7342 Mon Sep 17 00:00:00 2001 From: Hari Krishna Date: Fri, 24 Jul 2026 23:57:44 +0530 Subject: [PATCH 20/20] feat: implement idempotency checks for node and branch additions in workflow --- shared.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/shared.go b/shared.go index 68c1164a..3b38052b 100644 --- a/shared.go +++ b/shared.go @@ -39245,6 +39245,50 @@ func findNodePosition(wf *Workflow, nodeID string) (string, int, error) { func opAddNodeWithMapping(ctx context.Context, user User, wf *Workflow, op *WorkflowOperation, tempIDMap map[string]string) error { + // IDEMPOTENCY: if this temp_id was already resolved in a prior agent loop, + // the node already exists in the workflow (loaded from DB). Skip the add and + // re-populate the tempIDMap entry so downstream add_branch ops still work. + if len(op.TempID) > 0 { + if _, alreadyMapped := tempIDMap[op.TempID]; alreadyMapped { + return nil + } + } + + // Always check by label for idempotency, because labels must be unique in a workflow. + // This prevents duplicates if the workflow was loaded from DB after a prior save, + // regardless of whether a temp_id was provided or not. + var minLabel string + if op.NodeType == "action" { + var minAct MinimalAction + if jsonErr := json.Unmarshal(op.Data, &minAct); jsonErr == nil { + minLabel = minAct.Label + } + } else if op.NodeType == "trigger" { + var minTrig MinimalTrigger + if jsonErr := json.Unmarshal(op.Data, &minTrig); jsonErr == nil { + minLabel = minTrig.Label + } + } + + if len(minLabel) > 0 { + for _, existing := range wf.Actions { + if strings.EqualFold(existing.Label, minLabel) { + if len(op.TempID) > 0 { + tempIDMap[op.TempID] = existing.ID + } + return nil + } + } + for _, existing := range wf.Triggers { + if strings.EqualFold(existing.Label, minLabel) { + if len(op.TempID) > 0 { + tempIDMap[op.TempID] = existing.ID + } + return nil + } + } + } + err := opAddNode(ctx, user, wf, op) if err != nil { return err @@ -39597,6 +39641,21 @@ func opAddBranchWithMapping(wf *Workflow, op *WorkflowOperation, tempIDMap map[s resolvedData, _ := json.Marshal(branchData) op.Data = resolvedData + // IDEMPOTENCY: if a branch between this source to destination already exists + // (e.g. a second agent loop re-issued the same add_branch), skip the add + // and re-map the temp_id so downstream ops still resolve correctly. + for _, existing := range wf.Branches { + if existing.SourceID == branchData.SourceID && existing.DestinationID == branchData.DestinationID { + if len(op.TempID) > 0 { + tempIDMap[op.TempID] = existing.ID + } + if len(op.ID) > 0 { + tempIDMap[op.ID] = existing.ID + } + return nil + } + } + err := opAddBranch(wf, op) if err != nil { return err