From a26b64bb03b97879d7e2cfc3eefa6415230e2f92 Mon Sep 17 00:00:00 2001
From: Peter Clement <peter@budibase.com>
Date: Fri, 18 Oct 2024 15:05:17 +0100
Subject: [PATCH 01/14] pass user through to automation context

---
 .../server/src/api/controllers/automation.ts  |  4 +++
 .../server/src/api/controllers/row/index.ts   | 25 +++++++++++---
 .../src/api/controllers/rowAction/run.ts      |  2 +-
 .../tests/scenarios/scenarios.spec.ts         | 34 +++++++++++++++++++
 packages/server/src/automations/triggers.ts   | 10 +++++-
 .../server/src/definitions/automations.ts     |  3 +-
 packages/server/src/events/BudibaseEmitter.ts |  7 ++--
 packages/server/src/events/utils.ts           |  6 +++-
 packages/server/src/sdk/app/rowActions.ts     |  9 ++++-
 packages/server/src/threads/automation.ts     |  5 ++-
 .../documents/app/automation/automation.ts    |  1 +
 packages/types/src/sdk/automations/index.ts   |  3 +-
 12 files changed, 95 insertions(+), 14 deletions(-)

diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts
index 6177868303..df23f9f3b7 100644
--- a/packages/server/src/api/controllers/automation.ts
+++ b/packages/server/src/api/controllers/automation.ts
@@ -13,6 +13,7 @@ import {
   UserCtx,
   DeleteAutomationResponse,
   FetchAutomationResponse,
+  User,
 } from "@budibase/types"
 import { getActionDefinitions as actionDefs } from "../../automations/actions"
 import sdk from "../../sdk"
@@ -159,6 +160,7 @@ export async function trigger(ctx: UserCtx) {
         automation,
         {
           fields: ctx.request.body.fields,
+          user: ctx.user as User,
           timeout:
             ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT,
         },
@@ -183,6 +185,7 @@ export async function trigger(ctx: UserCtx) {
     await triggers.externalTrigger(automation, {
       ...ctx.request.body,
       appId: ctx.appId,
+      user: ctx.user as User,
     })
     ctx.body = {
       message: `Automation ${automation._id} has been triggered.`,
@@ -212,6 +215,7 @@ export async function test(ctx: UserCtx) {
     {
       ...testInput,
       appId: ctx.appId,
+      user: ctx.user,
     },
     { getResponses: true }
   )
diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts
index 60775ce628..67502f8b36 100644
--- a/packages/server/src/api/controllers/row/index.ts
+++ b/packages/server/src/api/controllers/row/index.ts
@@ -65,7 +65,14 @@ export async function patch(
     }
     ctx.status = 200
     ctx.eventEmitter &&
-      ctx.eventEmitter.emitRow(`row:update`, appId, row, table, oldRow)
+      ctx.eventEmitter.emitRow(
+        `row:update`,
+        appId,
+        row,
+        table,
+        oldRow,
+        ctx.user
+      )
     ctx.message = `${table.name} updated successfully.`
     ctx.body = row
     gridSocket?.emitRowUpdate(ctx, row)
@@ -96,7 +103,8 @@ export const save = async (ctx: UserCtx<Row, Row>) => {
         sdk.rows.save(sourceId, ctx.request.body, ctx.user?._id)
       )
   ctx.status = 200
-  ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
+  ctx.eventEmitter &&
+    ctx.eventEmitter.emitRow(`row:save`, appId, row, table, null, ctx.user)
   ctx.message = `${table.name} saved successfully`
   // prefer squashed for response
   ctx.body = row || squashed
@@ -168,7 +176,8 @@ async function deleteRows(ctx: UserCtx<DeleteRowRequest>) {
   }
 
   for (let row of rows) {
-    ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
+    ctx.eventEmitter &&
+      ctx.eventEmitter.emitRow(`row:delete`, appId, row, null, null, ctx.user)
     gridSocket?.emitRowDeletion(ctx, row)
   }
 
@@ -184,7 +193,15 @@ async function deleteRow(ctx: UserCtx<DeleteRowRequest>) {
     await quotas.removeRow()
   }
 
-  ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, resp.row)
+  ctx.eventEmitter &&
+    ctx.eventEmitter.emitRow(
+      `row:delete`,
+      appId,
+      resp.row,
+      null,
+      null,
+      ctx.user
+    )
   gridSocket?.emitRowDeletion(ctx, resp.row)
 
   return resp
diff --git a/packages/server/src/api/controllers/rowAction/run.ts b/packages/server/src/api/controllers/rowAction/run.ts
index c4b6a276bf..05bdb7bace 100644
--- a/packages/server/src/api/controllers/rowAction/run.ts
+++ b/packages/server/src/api/controllers/rowAction/run.ts
@@ -5,6 +5,6 @@ export async function run(ctx: Ctx<RowActionTriggerRequest, void>) {
   const { tableId, actionId } = ctx.params
   const { rowId } = ctx.request.body
 
-  await sdk.rowActions.run(tableId, actionId, rowId)
+  await sdk.rowActions.run(tableId, actionId, rowId, ctx.user)
   ctx.status = 200
 }
diff --git a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts
index 850a04a95a..358aba35c8 100644
--- a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts
+++ b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts
@@ -482,4 +482,38 @@ describe("Automation Scenarios", () => {
       }
     )
   })
+
+  it("Check user is passed through from row trigger", async () => {
+    const table = await config.createTable()
+
+    const builder = createAutomationBuilder({
+      name: "Test a user is successfully passed from the trigger",
+    })
+
+    const results = await builder
+      .rowUpdated(
+        { tableId: table._id! },
+        {
+          row: { name: "Test", description: "TEST" },
+          id: "1234",
+        }
+      )
+      .serverLog({ text: "{{ [user].[email] }}" })
+      .run()
+
+    expect(results.steps[0].outputs.message).toContain("example.com")
+  })
+
+  it("Check user is passed through from app trigger", async () => {
+    const builder = createAutomationBuilder({
+      name: "Test a user is successfully passed from the trigger",
+    })
+
+    const results = await builder
+      .appAction({ fields: {} })
+      .serverLog({ text: "{{ [user].[email] }}" })
+      .run()
+
+    expect(results.steps[0].outputs.message).toContain("example.com")
+  })
 })
diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts
index 110ccfa37a..efcbf27644 100644
--- a/packages/server/src/automations/triggers.ts
+++ b/packages/server/src/automations/triggers.ts
@@ -19,6 +19,7 @@ import {
   AutomationStoppedReason,
   AutomationStatus,
   AutomationRowEvent,
+  User,
 } from "@budibase/types"
 import { executeInThread } from "../threads/automation"
 import { dataFilters, sdk } from "@budibase/shared-core"
@@ -140,9 +141,15 @@ function rowPassesFilters(row: Row, filters: SearchFilters) {
 
 export async function externalTrigger(
   automation: Automation,
-  params: { fields: Record<string, any>; timeout?: number; appId?: string },
+  params: {
+    fields: Record<string, any>
+    timeout?: number
+    appId?: string
+    user?: User
+  },
   { getResponses }: { getResponses?: boolean } = {}
 ): Promise<any> {
+  console.log("user: " + params.user)
   if (automation.disabled) {
     throw new Error("Automation is disabled")
   }
@@ -189,6 +196,7 @@ export async function externalTrigger(
       appId: context.getAppId(),
       automation,
     }
+    console.log(data)
     return executeInThread({ data } as AutomationJob)
   } else {
     return automationQueue.add(data, JOB_OPTS)
diff --git a/packages/server/src/definitions/automations.ts b/packages/server/src/definitions/automations.ts
index 9433075da7..70faed9327 100644
--- a/packages/server/src/definitions/automations.ts
+++ b/packages/server/src/definitions/automations.ts
@@ -1,4 +1,4 @@
-import { AutomationResults, LoopStepType } from "@budibase/types"
+import { AutomationResults, LoopStepType, User } from "@budibase/types"
 
 export interface LoopInput {
   option: LoopStepType
@@ -18,5 +18,6 @@ export interface AutomationContext extends AutomationResults {
   stepsById: Record<string, any>
   stepsByName: Record<string, any>
   env?: Record<string, string>
+  user?: User
   trigger: any
 }
diff --git a/packages/server/src/events/BudibaseEmitter.ts b/packages/server/src/events/BudibaseEmitter.ts
index 8feb36bbf5..d09ea20a5d 100644
--- a/packages/server/src/events/BudibaseEmitter.ts
+++ b/packages/server/src/events/BudibaseEmitter.ts
@@ -1,6 +1,6 @@
 import { EventEmitter } from "events"
 import { rowEmission, tableEmission } from "./utils"
-import { Table, Row } from "@budibase/types"
+import { Table, Row, User } from "@budibase/types"
 
 /**
  * keeping event emitter in one central location as it might be used for things other than
@@ -18,9 +18,10 @@ class BudibaseEmitter extends EventEmitter {
     appId: string,
     row: Row,
     table?: Table,
-    oldRow?: Row
+    oldRow?: Row,
+    user?: User
   ) {
-    rowEmission({ emitter: this, eventName, appId, row, table, oldRow })
+    rowEmission({ emitter: this, eventName, appId, row, table, oldRow, user })
   }
 
   emitTable(eventName: string, appId: string, table?: Table) {
diff --git a/packages/server/src/events/utils.ts b/packages/server/src/events/utils.ts
index b972c8e473..5e4a1bebbf 100644
--- a/packages/server/src/events/utils.ts
+++ b/packages/server/src/events/utils.ts
@@ -1,4 +1,4 @@
-import { Table, Row } from "@budibase/types"
+import { Table, Row, User } from "@budibase/types"
 import BudibaseEmitter from "./BudibaseEmitter"
 
 type BBEventOpts = {
@@ -9,6 +9,7 @@ type BBEventOpts = {
   row?: Row
   oldRow?: Row
   metadata?: any
+  user?: User
 }
 
 interface BBEventTable extends Table {
@@ -24,6 +25,7 @@ type BBEvent = {
   id?: string
   revision?: string
   metadata?: any
+  user?: User
 }
 
 export function rowEmission({
@@ -34,12 +36,14 @@ export function rowEmission({
   table,
   metadata,
   oldRow,
+  user,
 }: BBEventOpts) {
   let event: BBEvent = {
     row,
     oldRow,
     appId,
     tableId: row?.tableId,
+    user,
   }
   if (table) {
     event.table = table
diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts
index 3d8a6fd9be..ed87133b5d 100644
--- a/packages/server/src/sdk/app/rowActions.ts
+++ b/packages/server/src/sdk/app/rowActions.ts
@@ -4,6 +4,7 @@ import {
   AutomationTriggerStepId,
   SEPARATOR,
   TableRowActions,
+  User,
   VirtualDocumentType,
 } from "@budibase/types"
 import { generateRowActionsID } from "../../db/utils"
@@ -236,7 +237,12 @@ export async function remove(tableId: string, rowActionId: string) {
   })
 }
 
-export async function run(tableId: any, rowActionId: any, rowId: string) {
+export async function run(
+  tableId: any,
+  rowActionId: any,
+  rowId: string,
+  user: User
+) {
   const table = await sdk.tables.getTable(tableId)
   if (!table) {
     throw new HTTPError("Table not found", 404)
@@ -258,6 +264,7 @@ export async function run(tableId: any, rowActionId: any, rowId: string) {
         row,
         table,
       },
+      user,
       appId: context.getAppId(),
     },
     { getResponses: true }
diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts
index 3b47634663..a75c0d2870 100644
--- a/packages/server/src/threads/automation.ts
+++ b/packages/server/src/threads/automation.ts
@@ -26,6 +26,7 @@ import {
   BranchStep,
   LoopStep,
   SearchFilters,
+  User,
 } from "@budibase/types"
 import { AutomationContext, TriggerOutput } from "../definitions/automations"
 import { WorkerCallback } from "./definitions"
@@ -75,6 +76,7 @@ class Orchestrator {
   private loopStepOutputs: LoopStep[]
   private stopped: boolean
   private executionOutput: Omit<AutomationContext, "stepsByName" | "stepsById">
+  private currentUser: User | undefined
 
   constructor(job: AutomationJob) {
     let automation = job.data.automation
@@ -106,6 +108,7 @@ class Orchestrator {
     this.updateExecutionOutput(triggerId, triggerStepId, null, triggerOutput)
     this.loopStepOutputs = []
     this.stopped = false
+    this.currentUser = triggerOutput.user
   }
 
   cleanupTriggerOutputs(stepId: string, triggerOutput: TriggerOutput) {
@@ -258,6 +261,7 @@ class Orchestrator {
           automationId: this.automation._id,
         })
         this.context.env = await sdkUtils.getEnvironmentVariables()
+        this.context.user = this.currentUser
 
         let metadata
 
@@ -572,7 +576,6 @@ class Orchestrator {
           originalStepInput,
           this.processContext(this.context)
         )
-
         inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs)
 
         const outputs = await stepFn({
diff --git a/packages/types/src/documents/app/automation/automation.ts b/packages/types/src/documents/app/automation/automation.ts
index effe99a328..1af892d8d1 100644
--- a/packages/types/src/documents/app/automation/automation.ts
+++ b/packages/types/src/documents/app/automation/automation.ts
@@ -261,6 +261,7 @@ export type UpdatedRowEventEmitter = {
   oldRow: Row
   table: Table
   appId: string
+  user: User
 }
 
 export enum LoopStepType {
diff --git a/packages/types/src/sdk/automations/index.ts b/packages/types/src/sdk/automations/index.ts
index d04f126c32..f5c57b54d8 100644
--- a/packages/types/src/sdk/automations/index.ts
+++ b/packages/types/src/sdk/automations/index.ts
@@ -1,4 +1,4 @@
-import { Automation, AutomationMetadata, Row } from "../../documents"
+import { Automation, AutomationMetadata, Row, User } from "../../documents"
 import { Job } from "bull"
 
 export interface AutomationDataEvent {
@@ -8,6 +8,7 @@ export interface AutomationDataEvent {
   timeout?: number
   row?: Row
   oldRow?: Row
+  user?: User
 }
 
 export interface AutomationData {

From 2b7742d96fba71cb16ee3741e9732ab35eddb476 Mon Sep 17 00:00:00 2001
From: Peter Clement <peter@budibase.com>
Date: Fri, 18 Oct 2024 15:12:26 +0100
Subject: [PATCH 02/14] pro

---
 packages/pro | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/pro b/packages/pro
index fc4c7f4925..1a749caba9 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit fc4c7f4925139af078480217965c3d6338dc0a7f
+Subproject commit 1a749caba9c85aab2645e5d00db479eb53d3f80f

From 4cabc09f8abb80fca08bab6375ea35f4353f43d1 Mon Sep 17 00:00:00 2001
From: Peter Clement <peter@budibase.com>
Date: Fri, 18 Oct 2024 15:35:31 +0100
Subject: [PATCH 03/14] fix row actions test

---
 packages/server/src/api/routes/tests/rowAction.spec.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index 7140f3486e..d3b90f6f6b 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -783,6 +783,7 @@ describe("/rowsActions", () => {
                 ...(await config.api.table.get(tableId)),
                 views: expect.anything(),
               },
+              user: expect.anything(),
               automation: expect.objectContaining({
                 _id: rowAction.automationId,
               }),

From 8f46119817b0fd6758e9c7b188571655ba9d86cb Mon Sep 17 00:00:00 2001
From: Peter Clement <peter@budibase.com>
Date: Mon, 21 Oct 2024 10:04:51 +0100
Subject: [PATCH 04/14] refs

---
 packages/account-portal | 2 +-
 packages/pro            | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/account-portal b/packages/account-portal
index 8cd052ce82..fedf9957c1 160000
--- a/packages/account-portal
+++ b/packages/account-portal
@@ -1 +1 @@
-Subproject commit 8cd052ce8288f343812a514d06c5a9459b3ba1a8
+Subproject commit fedf9957c1acc240b1a3bccb7882d7d763d8f499
diff --git a/packages/pro b/packages/pro
index 1a749caba9..297fdc937e 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit 1a749caba9c85aab2645e5d00db479eb53d3f80f
+Subproject commit 297fdc937e9c650b4964fc1a942b60022b195865

From 09695fabd6983dcbab5f39bd1e8ab0d7383172c3 Mon Sep 17 00:00:00 2001
From: Peter Clement <peter@budibase.com>
Date: Tue, 22 Oct 2024 10:52:52 +0100
Subject: [PATCH 05/14] extract necessary user bindings and add types

---
 .../server/src/api/controllers/automation.ts  |  7 +++---
 .../server/src/api/controllers/row/index.ts   | 22 +++++++++++++++----
 packages/server/src/automations/triggers.ts   |  6 ++---
 .../server/src/definitions/automations.ts     |  4 ++--
 packages/server/src/sdk/users/utils.ts        |  3 ++-
 packages/server/src/threads/automation.ts     |  4 ++--
 packages/types/src/documents/global/user.ts   | 10 +++++++++
 packages/types/src/sdk/automations/index.ts   |  9 ++++++--
 8 files changed, 46 insertions(+), 19 deletions(-)

diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts
index df23f9f3b7..b19218647b 100644
--- a/packages/server/src/api/controllers/automation.ts
+++ b/packages/server/src/api/controllers/automation.ts
@@ -13,7 +13,6 @@ import {
   UserCtx,
   DeleteAutomationResponse,
   FetchAutomationResponse,
-  User,
 } from "@budibase/types"
 import { getActionDefinitions as actionDefs } from "../../automations/actions"
 import sdk from "../../sdk"
@@ -160,7 +159,7 @@ export async function trigger(ctx: UserCtx) {
         automation,
         {
           fields: ctx.request.body.fields,
-          user: ctx.user as User,
+          user: sdk.users.getUserContextBindings(ctx.user),
           timeout:
             ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT,
         },
@@ -185,7 +184,7 @@ export async function trigger(ctx: UserCtx) {
     await triggers.externalTrigger(automation, {
       ...ctx.request.body,
       appId: ctx.appId,
-      user: ctx.user as User,
+      user: sdk.users.getUserContextBindings(ctx.user),
     })
     ctx.body = {
       message: `Automation ${automation._id} has been triggered.`,
@@ -215,7 +214,7 @@ export async function test(ctx: UserCtx) {
     {
       ...testInput,
       appId: ctx.appId,
-      user: ctx.user,
+      user: sdk.users.getUserContextBindings(ctx.user),
     },
     { getResponses: true }
   )
diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts
index 67502f8b36..33599a0119 100644
--- a/packages/server/src/api/controllers/row/index.ts
+++ b/packages/server/src/api/controllers/row/index.ts
@@ -71,7 +71,7 @@ export async function patch(
         row,
         table,
         oldRow,
-        ctx.user
+        sdk.users.getUserContextBindings(ctx.user)
       )
     ctx.message = `${table.name} updated successfully.`
     ctx.body = row
@@ -104,7 +104,14 @@ export const save = async (ctx: UserCtx<Row, Row>) => {
       )
   ctx.status = 200
   ctx.eventEmitter &&
-    ctx.eventEmitter.emitRow(`row:save`, appId, row, table, null, ctx.user)
+    ctx.eventEmitter.emitRow(
+      `row:save`,
+      appId,
+      row,
+      table,
+      null,
+      sdk.users.getUserContextBindings(ctx.user)
+    )
   ctx.message = `${table.name} saved successfully`
   // prefer squashed for response
   ctx.body = row || squashed
@@ -177,7 +184,14 @@ async function deleteRows(ctx: UserCtx<DeleteRowRequest>) {
 
   for (let row of rows) {
     ctx.eventEmitter &&
-      ctx.eventEmitter.emitRow(`row:delete`, appId, row, null, null, ctx.user)
+      ctx.eventEmitter.emitRow(
+        `row:delete`,
+        appId,
+        row,
+        null,
+        null,
+        sdk.users.getUserContextBindings(ctx.user)
+      )
     gridSocket?.emitRowDeletion(ctx, row)
   }
 
@@ -200,7 +214,7 @@ async function deleteRow(ctx: UserCtx<DeleteRowRequest>) {
       resp.row,
       null,
       null,
-      ctx.user
+      sdk.users.getUserContextBindings(ctx.user)
     )
   gridSocket?.emitRowDeletion(ctx, resp.row)
 
diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts
index efcbf27644..e3b7318211 100644
--- a/packages/server/src/automations/triggers.ts
+++ b/packages/server/src/automations/triggers.ts
@@ -19,7 +19,7 @@ import {
   AutomationStoppedReason,
   AutomationStatus,
   AutomationRowEvent,
-  User,
+  UserBindings,
 } from "@budibase/types"
 import { executeInThread } from "../threads/automation"
 import { dataFilters, sdk } from "@budibase/shared-core"
@@ -145,11 +145,10 @@ export async function externalTrigger(
     fields: Record<string, any>
     timeout?: number
     appId?: string
-    user?: User
+    user?: UserBindings | undefined
   },
   { getResponses }: { getResponses?: boolean } = {}
 ): Promise<any> {
-  console.log("user: " + params.user)
   if (automation.disabled) {
     throw new Error("Automation is disabled")
   }
@@ -196,7 +195,6 @@ export async function externalTrigger(
       appId: context.getAppId(),
       automation,
     }
-    console.log(data)
     return executeInThread({ data } as AutomationJob)
   } else {
     return automationQueue.add(data, JOB_OPTS)
diff --git a/packages/server/src/definitions/automations.ts b/packages/server/src/definitions/automations.ts
index 70faed9327..d551584fd1 100644
--- a/packages/server/src/definitions/automations.ts
+++ b/packages/server/src/definitions/automations.ts
@@ -1,4 +1,4 @@
-import { AutomationResults, LoopStepType, User } from "@budibase/types"
+import { AutomationResults, LoopStepType, UserBindings } from "@budibase/types"
 
 export interface LoopInput {
   option: LoopStepType
@@ -18,6 +18,6 @@ export interface AutomationContext extends AutomationResults {
   stepsById: Record<string, any>
   stepsByName: Record<string, any>
   env?: Record<string, string>
-  user?: User
+  user?: UserBindings
   trigger: any
 }
diff --git a/packages/server/src/sdk/users/utils.ts b/packages/server/src/sdk/users/utils.ts
index 74389a1444..8266d3e28a 100644
--- a/packages/server/src/sdk/users/utils.ts
+++ b/packages/server/src/sdk/users/utils.ts
@@ -12,6 +12,7 @@ import {
   UserMetadata,
   Database,
   ContextUserMetadata,
+  UserBindings,
 } from "@budibase/types"
 
 export function combineMetadataAndUser(
@@ -125,7 +126,7 @@ export async function syncGlobalUsers() {
   }
 }
 
-export function getUserContextBindings(user: ContextUser) {
+export function getUserContextBindings(user: ContextUser): UserBindings {
   if (!user) {
     return {}
   }
diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts
index a75c0d2870..0f17b44424 100644
--- a/packages/server/src/threads/automation.ts
+++ b/packages/server/src/threads/automation.ts
@@ -26,7 +26,7 @@ import {
   BranchStep,
   LoopStep,
   SearchFilters,
-  User,
+  UserBindings,
 } from "@budibase/types"
 import { AutomationContext, TriggerOutput } from "../definitions/automations"
 import { WorkerCallback } from "./definitions"
@@ -76,7 +76,7 @@ class Orchestrator {
   private loopStepOutputs: LoopStep[]
   private stopped: boolean
   private executionOutput: Omit<AutomationContext, "stepsByName" | "stepsById">
-  private currentUser: User | undefined
+  private currentUser: UserBindings | undefined
 
   constructor(job: AutomationJob) {
     let automation = job.data.automation
diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts
index a1c5b2506f..85641bf4c5 100644
--- a/packages/types/src/documents/global/user.ts
+++ b/packages/types/src/documents/global/user.ts
@@ -68,6 +68,16 @@ export interface User extends Document {
   appSort?: string
 }
 
+export interface UserBindings extends Document {
+  firstName?: string
+  lastName?: string
+  email?: string
+  status?: string
+  roleId?: string | undefined | null
+  globalId?: string
+  userId?: string
+}
+
 export enum UserStatus {
   ACTIVE = "active",
   INACTIVE = "inactive",
diff --git a/packages/types/src/sdk/automations/index.ts b/packages/types/src/sdk/automations/index.ts
index f5c57b54d8..9ceded03ee 100644
--- a/packages/types/src/sdk/automations/index.ts
+++ b/packages/types/src/sdk/automations/index.ts
@@ -1,4 +1,9 @@
-import { Automation, AutomationMetadata, Row, User } from "../../documents"
+import {
+  Automation,
+  AutomationMetadata,
+  Row,
+  UserBindings,
+} from "../../documents"
 import { Job } from "bull"
 
 export interface AutomationDataEvent {
@@ -8,7 +13,7 @@ export interface AutomationDataEvent {
   timeout?: number
   row?: Row
   oldRow?: Row
-  user?: User
+  user?: UserBindings
 }
 
 export interface AutomationData {

From 613e63ccbfdc981029b7e742c20e561a19e1aa0e Mon Sep 17 00:00:00 2001
From: Peter Clement <peter@budibase.com>
Date: Tue, 22 Oct 2024 10:58:24 +0100
Subject: [PATCH 06/14] remove update ref

---
 packages/account-portal | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/account-portal b/packages/account-portal
index fedf9957c1..b5ba3189d5 160000
--- a/packages/account-portal
+++ b/packages/account-portal
@@ -1 +1 @@
-Subproject commit fedf9957c1acc240b1a3bccb7882d7d763d8f499
+Subproject commit b5ba3189d508769ace587ac58008421800a4a316

From 33ea5f09a71236fb0c327089ae55a5e5198d2cd1 Mon Sep 17 00:00:00 2001
From: mike12345567 <me@michaeldrury.co.uk>
Date: Tue, 22 Oct 2024 11:06:46 +0100
Subject: [PATCH 07/14] Revert account portal ref.

---
 packages/account-portal | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/account-portal b/packages/account-portal
index b5ba3189d5..8cd052ce82 160000
--- a/packages/account-portal
+++ b/packages/account-portal
@@ -1 +1 @@
-Subproject commit b5ba3189d508769ace587ac58008421800a4a316
+Subproject commit 8cd052ce8288f343812a514d06c5a9459b3ba1a8

From bce430b57c0f6f68e75d25ab744244bc3260a0b8 Mon Sep 17 00:00:00 2001
From: Peter Clement <peter@budibase.com>
Date: Tue, 22 Oct 2024 12:03:16 +0100
Subject: [PATCH 08/14] pr comments

---
 .../server/src/api/controllers/row/index.ts   | 40 ++++++++-----------
 packages/server/src/automations/triggers.ts   |  2 +-
 packages/server/src/events/BudibaseEmitter.ts | 23 +++++++----
 packages/types/src/documents/global/user.ts   |  2 +-
 4 files changed, 34 insertions(+), 33 deletions(-)

diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts
index 33599a0119..4d40476b7c 100644
--- a/packages/server/src/api/controllers/row/index.ts
+++ b/packages/server/src/api/controllers/row/index.ts
@@ -65,14 +65,14 @@ export async function patch(
     }
     ctx.status = 200
     ctx.eventEmitter &&
-      ctx.eventEmitter.emitRow(
-        `row:update`,
+      ctx.eventEmitter.emitRow({
+        eventName: `row:update`,
         appId,
         row,
         table,
         oldRow,
-        sdk.users.getUserContextBindings(ctx.user)
-      )
+        user: sdk.users.getUserContextBindings(ctx.user),
+      })
     ctx.message = `${table.name} updated successfully.`
     ctx.body = row
     gridSocket?.emitRowUpdate(ctx, row)
@@ -104,14 +104,13 @@ export const save = async (ctx: UserCtx<Row, Row>) => {
       )
   ctx.status = 200
   ctx.eventEmitter &&
-    ctx.eventEmitter.emitRow(
-      `row:save`,
+    ctx.eventEmitter.emitRow({
+      eventName: `row:save`,
       appId,
       row,
       table,
-      null,
-      sdk.users.getUserContextBindings(ctx.user)
-    )
+      user: sdk.users.getUserContextBindings(ctx.user),
+    })
   ctx.message = `${table.name} saved successfully`
   // prefer squashed for response
   ctx.body = row || squashed
@@ -184,17 +183,14 @@ async function deleteRows(ctx: UserCtx<DeleteRowRequest>) {
 
   for (let row of rows) {
     ctx.eventEmitter &&
-      ctx.eventEmitter.emitRow(
-        `row:delete`,
+      ctx.eventEmitter.emitRow({
+        eventName: `row:delete`,
         appId,
         row,
-        null,
-        null,
-        sdk.users.getUserContextBindings(ctx.user)
-      )
+        user: sdk.users.getUserContextBindings(ctx.user),
+      })
     gridSocket?.emitRowDeletion(ctx, row)
   }
-
   return rows
 }
 
@@ -208,14 +204,12 @@ async function deleteRow(ctx: UserCtx<DeleteRowRequest>) {
   }
 
   ctx.eventEmitter &&
-    ctx.eventEmitter.emitRow(
-      `row:delete`,
+    ctx.eventEmitter.emitRow({
+      eventName: `row:delete`,
       appId,
-      resp.row,
-      null,
-      null,
-      sdk.users.getUserContextBindings(ctx.user)
-    )
+      row: resp.row,
+      user: sdk.users.getUserContextBindings(ctx.user),
+    })
   gridSocket?.emitRowDeletion(ctx, resp.row)
 
   return resp
diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts
index e3b7318211..70fda1f237 100644
--- a/packages/server/src/automations/triggers.ts
+++ b/packages/server/src/automations/triggers.ts
@@ -145,7 +145,7 @@ export async function externalTrigger(
     fields: Record<string, any>
     timeout?: number
     appId?: string
-    user?: UserBindings | undefined
+    user?: UserBindings
   },
   { getResponses }: { getResponses?: boolean } = {}
 ): Promise<any> {
diff --git a/packages/server/src/events/BudibaseEmitter.ts b/packages/server/src/events/BudibaseEmitter.ts
index d09ea20a5d..c8983096d0 100644
--- a/packages/server/src/events/BudibaseEmitter.ts
+++ b/packages/server/src/events/BudibaseEmitter.ts
@@ -13,14 +13,21 @@ import { Table, Row, User } from "@budibase/types"
  * This is specifically quite important for template strings used in automations.
  */
 class BudibaseEmitter extends EventEmitter {
-  emitRow(
-    eventName: string,
-    appId: string,
-    row: Row,
-    table?: Table,
-    oldRow?: Row,
-    user?: User
-  ) {
+  emitRow({
+    eventName,
+    appId,
+    row,
+    table,
+    oldRow,
+    user,
+  }: {
+    eventName: string
+    appId: string
+    row: Row
+    table?: Table
+    oldRow?: Row
+    user: User
+  }) {
     rowEmission({ emitter: this, eventName, appId, row, table, oldRow, user })
   }
 
diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts
index 85641bf4c5..829a171843 100644
--- a/packages/types/src/documents/global/user.ts
+++ b/packages/types/src/documents/global/user.ts
@@ -73,7 +73,7 @@ export interface UserBindings extends Document {
   lastName?: string
   email?: string
   status?: string
-  roleId?: string | undefined | null
+  roleId?: string | null
   globalId?: string
   userId?: string
 }

From e67126e6b69898f99cb4c1e27a3f01610b3e07e4 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Tue, 22 Oct 2024 14:30:22 +0200
Subject: [PATCH 09/14] Navigate to automation on creation

---
 .../automation/AutomationPanel/CreateAutomationModal.svelte   | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
index 365d3d358f..35fdba970a 100644
--- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
@@ -1,4 +1,5 @@
 <script>
+  import { goto } from "@roxi/routify"
   import { automationStore } from "stores/builder"
   import {
     notifications,
@@ -32,11 +33,12 @@
         triggerVal.stepId,
         triggerVal
       )
-      await automationStore.actions.create(name, trigger)
+      const automation = await automationStore.actions.create(name, trigger)
       if (triggerVal.stepId === TriggerStepID.WEBHOOK) {
         webhookModal.show()
       }
       notifications.success(`Automation ${name} created`)
+      $goto(`../automation/${automation._id}`)
     } catch (error) {
       notifications.error("Error creating automation")
     }

From 75f17f5c1251a9d5b7d4b8cd077063894528ea51 Mon Sep 17 00:00:00 2001
From: Peter Clement <peter@budibase.com>
Date: Tue, 22 Oct 2024 13:53:31 +0100
Subject: [PATCH 10/14] update test to check for user id

---
 packages/server/src/api/routes/tests/rowAction.spec.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index d3b90f6f6b..9f9a33c73b 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -767,7 +767,6 @@ describe("/rowsActions", () => {
     it("can trigger an automation given valid data", async () => {
       expect(await getAutomationLogs()).toBeEmpty()
       await config.api.rowAction.trigger(viewId, rowAction.id, { rowId })
-
       const automationLogs = await getAutomationLogs()
       expect(automationLogs).toEqual([
         expect.objectContaining({
@@ -783,7 +782,10 @@ describe("/rowsActions", () => {
                 ...(await config.api.table.get(tableId)),
                 views: expect.anything(),
               },
-              user: expect.anything(),
+              user: expect.objectContaining({
+                _id: "ro_ta_users_" + config.getUser()._id,
+              }),
+
               automation: expect.objectContaining({
                 _id: rowAction.automationId,
               }),

From 94ebd7c6ef300b93441f9a865c9ffe96239371b6 Mon Sep 17 00:00:00 2001
From: Peter Clement <peter@budibase.com>
Date: Tue, 22 Oct 2024 14:36:15 +0100
Subject: [PATCH 11/14] update automation emitter

---
 packages/server/src/events/AutomationEmitter.ts | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/packages/server/src/events/AutomationEmitter.ts b/packages/server/src/events/AutomationEmitter.ts
index 32fd130929..a63273bdc0 100644
--- a/packages/server/src/events/AutomationEmitter.ts
+++ b/packages/server/src/events/AutomationEmitter.ts
@@ -31,7 +31,17 @@ class AutomationEmitter {
     }
   }
 
-  async emitRow(eventName: string, appId: string, row: Row, table?: Table) {
+  async emitRow({
+    eventName,
+    appId,
+    row,
+    table,
+  }: {
+    eventName: string
+    appId: string
+    row: Row
+    table?: Table
+  }) {
     let MAX_AUTOMATION_CHAIN = await this.getMaxAutomationChain()
 
     // don't emit even if we've reached max automation chain

From 06670ba549cebe46427e34edea669e0591af0103 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Tue, 22 Oct 2024 17:32:32 +0200
Subject: [PATCH 12/14] Add local prerelease to version locally

---
 packages/backend-core/src/environment.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts
index 4e93e8d9ee..7d3a9f18f5 100644
--- a/packages/backend-core/src/environment.ts
+++ b/packages/backend-core/src/environment.ts
@@ -83,7 +83,7 @@ function getPackageJsonFields(): {
   if (isDev() && !isTest()) {
     try {
       const lerna = getParentFile("lerna.json")
-      localVersion = lerna.version
+      localVersion = `${lerna.version}+local`
     } catch {
       //
     }

From d73643f0b3eefb22ebbf0b7f63902ac7bf71a3dd Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Tue, 22 Oct 2024 17:42:36 +0200
Subject: [PATCH 13/14] Allow serving old versions locally

---
 .../server/src/api/controllers/static/index.ts  | 15 ++++++++++-----
 packages/server/src/constants/index.ts          |  2 --
 packages/server/src/utilities/fileSystem/app.ts |  6 +++---
 .../src/utilities/fileSystem/clientLibrary.ts   | 17 +++++++++++++++++
 4 files changed, 30 insertions(+), 10 deletions(-)

diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts
index b4b4fdea5d..307867601b 100644
--- a/packages/server/src/api/controllers/static/index.ts
+++ b/packages/server/src/api/controllers/static/index.ts
@@ -2,11 +2,12 @@ import { InvalidFileExtensions } from "@budibase/shared-core"
 import AppComponent from "./templates/BudibaseApp.svelte"
 import { join } from "../../../utilities/centralPath"
 import * as uuid from "uuid"
-import { devClientVersion, ObjectStoreBuckets } from "../../../constants"
+import { ObjectStoreBuckets } from "../../../constants"
 import { processString } from "@budibase/string-templates"
 import {
   loadHandlebarsFile,
   NODE_MODULES_PATH,
+  shouldServeLocally,
   TOP_LEVEL_PATH,
 } from "../../../utilities/fileSystem"
 import env from "../../../environment"
@@ -257,25 +258,29 @@ export const serveBuilderPreview = async function (ctx: Ctx) {
 export const serveClientLibrary = async function (ctx: Ctx) {
   const version = ctx.request.query.version
 
+  if (Array.isArray(version)) {
+    ctx.throw(400)
+  }
+
   const appId = context.getAppId() || (ctx.request.query.appId as string)
   let rootPath = join(NODE_MODULES_PATH, "@budibase", "client", "dist")
   if (!appId) {
     ctx.throw(400, "No app ID provided - cannot fetch client library.")
   }
-  if (env.isProd() || (env.isDev() && version !== devClientVersion)) {
+
+  const serverLocally = shouldServeLocally(version || "")
+  if (!serverLocally) {
     ctx.body = await objectStore.getReadStream(
       ObjectStoreBuckets.APPS,
       objectStore.clientLibraryPath(appId!)
     )
     ctx.set("Content-Type", "application/javascript")
-  } else if (env.isDev() && version === devClientVersion) {
+  } else {
     // incase running from TS directly
     const tsPath = join(require.resolve("@budibase/client"), "..")
     return send(ctx, "budibase-client.js", {
       root: !fs.existsSync(rootPath) ? tsPath : rootPath,
     })
-  } else {
-    ctx.throw(500, "Unable to retrieve client library.")
   }
 }
 
diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts
index 316d27b3fc..bac838b53e 100644
--- a/packages/server/src/constants/index.ts
+++ b/packages/server/src/constants/index.ts
@@ -152,8 +152,6 @@ export enum AutomationErrors {
   FAILURE_CONDITION = "FAILURE_CONDITION_MET",
 }
 
-export const devClientVersion = "0.0.0"
-
 // pass through the list from the auth/core lib
 export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets
 export const MAX_AUTOMATION_RECURRING_ERRORS = 5
diff --git a/packages/server/src/utilities/fileSystem/app.ts b/packages/server/src/utilities/fileSystem/app.ts
index c708a9422b..9bd88ba0b1 100644
--- a/packages/server/src/utilities/fileSystem/app.ts
+++ b/packages/server/src/utilities/fileSystem/app.ts
@@ -1,8 +1,8 @@
 import { budibaseTempDir } from "../budibaseDir"
 import fs from "fs"
 import { join } from "path"
-import { ObjectStoreBuckets, devClientVersion } from "../../constants"
-import { updateClientLibrary } from "./clientLibrary"
+import { ObjectStoreBuckets } from "../../constants"
+import { shouldServeLocally, updateClientLibrary } from "./clientLibrary"
 import env from "../../environment"
 import { objectStore, context } from "@budibase/backend-core"
 import { TOP_LEVEL_PATH } from "./filesystem"
@@ -40,7 +40,7 @@ export const getComponentLibraryManifest = async (library: string) => {
     const db = context.getAppDB()
     const app = await db.get<App>(DocumentType.APP_METADATA)
 
-    if (app.version === devClientVersion || env.isTest()) {
+    if (shouldServeLocally(app.version) || env.isTest()) {
       const paths = [
         join(TOP_LEVEL_PATH, "packages/client", filename),
         join(process.cwd(), "client", filename),
diff --git a/packages/server/src/utilities/fileSystem/clientLibrary.ts b/packages/server/src/utilities/fileSystem/clientLibrary.ts
index c994502995..faa328fdc5 100644
--- a/packages/server/src/utilities/fileSystem/clientLibrary.ts
+++ b/packages/server/src/utilities/fileSystem/clientLibrary.ts
@@ -1,3 +1,4 @@
+import semver from "semver"
 import path, { join } from "path"
 import { ObjectStoreBuckets } from "../../constants"
 import fs from "fs"
@@ -183,3 +184,19 @@ export async function revertClientLibrary(appId: string) {
 
   return JSON.parse(await manifestSrc)
 }
+
+export function shouldServeLocally(version: string) {
+  if (env.isProd() || !env.isDev()) {
+    return false
+  }
+
+  if (version === "0.0.0") {
+    return true
+  }
+
+  const parsedSemver = semver.parse(version)
+  if (parsedSemver?.build?.[0] === "local") {
+    return true
+  }
+  return false
+}

From 91e3c87a9c56fa1fa1848ad0e3fbb7dbc687ca21 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Tue, 22 Oct 2024 17:58:51 +0200
Subject: [PATCH 14/14] Fix typo

---
 packages/server/src/api/controllers/static/index.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts
index 307867601b..1d04811019 100644
--- a/packages/server/src/api/controllers/static/index.ts
+++ b/packages/server/src/api/controllers/static/index.ts
@@ -268,8 +268,8 @@ export const serveClientLibrary = async function (ctx: Ctx) {
     ctx.throw(400, "No app ID provided - cannot fetch client library.")
   }
 
-  const serverLocally = shouldServeLocally(version || "")
-  if (!serverLocally) {
+  const serveLocally = shouldServeLocally(version || "")
+  if (!serveLocally) {
     ctx.body = await objectStore.getReadStream(
       ObjectStoreBuckets.APPS,
       objectStore.clientLibraryPath(appId!)