From eec0167a207f32ae1e46613abfd662aadbae8c20 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Tue, 4 Mar 2025 14:21:28 +0100 Subject: [PATCH 1/2] feat: add action success events to db --- .../migration.sql | 10 ++++++++++ core/prisma/schema/schema.dbml | 2 ++ core/prisma/schema/schema.prisma | 2 ++ packages/db/src/public/Event.ts | 2 ++ 4 files changed, 16 insertions(+) create mode 100644 core/prisma/migrations/20250304132049_add_action_success_events/migration.sql diff --git a/core/prisma/migrations/20250304132049_add_action_success_events/migration.sql b/core/prisma/migrations/20250304132049_add_action_success_events/migration.sql new file mode 100644 index 000000000..fa7125100 --- /dev/null +++ b/core/prisma/migrations/20250304132049_add_action_success_events/migration.sql @@ -0,0 +1,10 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "Event" ADD VALUE 'actionSucceeded'; +ALTER TYPE "Event" ADD VALUE 'actionFailed'; diff --git a/core/prisma/schema/schema.dbml b/core/prisma/schema/schema.dbml index a649027fd..d1a8ebb25 100644 --- a/core/prisma/schema/schema.dbml +++ b/core/prisma/schema/schema.dbml @@ -509,6 +509,8 @@ Enum Event { pubEnteredStage pubLeftStage pubInStageForDuration + actionSucceeded + actionFailed } Enum FormAccessType { diff --git a/core/prisma/schema/schema.prisma b/core/prisma/schema/schema.prisma index 55db04e39..bfda6e0d1 100644 --- a/core/prisma/schema/schema.prisma +++ b/core/prisma/schema/schema.prisma @@ -464,6 +464,8 @@ enum Event { pubEnteredStage pubLeftStage pubInStageForDuration + actionSucceeded + actionFailed } enum FormAccessType { diff --git a/packages/db/src/public/Event.ts b/packages/db/src/public/Event.ts index 17473ee3d..46605ae97 100644 --- a/packages/db/src/public/Event.ts +++ b/packages/db/src/public/Event.ts @@ -8,6 +8,8 @@ export enum Event { pubEnteredStage = "pubEnteredStage", pubLeftStage = "pubLeftStage", pubInStageForDuration = "pubInStageForDuration", + actionSucceeded = "actionSucceeded", + actionFailed = "actionFailed", } /** Zod schema for Event */ From 4ce4eb84700b817706d02f06fc9c9d876a2da8bb Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Wed, 5 Mar 2025 18:24:00 +0100 Subject: [PATCH 2/2] feat: setup basic chain actions --- core/actions/_lib/rules.ts | 26 +- core/actions/_lib/zodTypes.ts | 21 ++ core/actions/api/index.ts | 25 +- core/actions/types.ts | 17 +- .../[communitySlug]/stages/manage/actions.ts | 17 +- .../panel/actionsTab/StagePanelRule.tsx | 43 ++- .../actionsTab/StagePanelRuleCreator.tsx | 290 +++++++++++++----- .../panel/actionsTab/StagePanelRules.tsx | 1 + core/app/components/pubs/PubEditor/helpers.ts | 8 + core/lib/db/queries.ts | 30 +- core/lib/server/rules.ts | 176 ++++++++++- .../migration.sql | 13 + core/prisma/schema/schema.dbml | 5 + core/prisma/schema/schema.prisma | 12 +- packages/db/src/public/PublicSchema.ts | 44 +-- packages/db/src/public/Rules.ts | 9 + packages/db/src/table-names.ts | 8 + packages/ui/package.json | 2 + .../ActionInstanceSelector.tsx | 49 +++ .../ActionInstanceSelectorInput.tsx | 8 + .../ActionInstancesContext.tsx | 48 +++ packages/ui/src/actionInstances/index.tsx | 2 + packages/ui/src/auto-form/config.ts | 3 + 23 files changed, 725 insertions(+), 132 deletions(-) create mode 100644 core/prisma/migrations/20250305135055_add_action_ref_to_rul/migration.sql create mode 100644 packages/ui/src/actionInstances/ActionInstanceSelector.tsx create mode 100644 packages/ui/src/actionInstances/ActionInstanceSelectorInput.tsx create mode 100644 packages/ui/src/actionInstances/ActionInstancesContext.tsx create mode 100644 packages/ui/src/actionInstances/index.tsx diff --git a/core/actions/_lib/rules.ts b/core/actions/_lib/rules.ts index 09077ac50..5247a0e04 100644 --- a/core/actions/_lib/rules.ts +++ b/core/actions/_lib/rules.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import type { ActionInstances } from "db/public"; import { Event } from "db/public"; import { defineRule } from "~/actions/types"; @@ -37,7 +38,30 @@ export const pubEnteredStage = defineRule({ }); export type PubEnteredStage = typeof pubEnteredStage; -export type Rules = PubInStageForDuration | PubLeftStage | PubEnteredStage; +export const actionSucceeded = defineRule({ + event: Event.actionSucceeded, + display: { + base: "a specific action succeeds", + withConfig: (actionInstance: ActionInstances) => `${actionInstance.name} succeeds`, + }, +}); +export type ActionSucceeded = typeof actionSucceeded; + +export const actionFailed = defineRule({ + event: Event.actionFailed, + display: { + base: "a specific action fails", + withConfig: (actionInstance) => `${actionInstance.name} fails`, + }, +}); +export type ActionFailed = typeof actionFailed; + +export type Rules = + | PubInStageForDuration + | PubLeftStage + | PubEnteredStage + | ActionSucceeded + | ActionFailed; export type RuleForEvent = Extract; diff --git a/core/actions/_lib/zodTypes.ts b/core/actions/_lib/zodTypes.ts index 7b9351aab..25f58d381 100644 --- a/core/actions/_lib/zodTypes.ts +++ b/core/actions/_lib/zodTypes.ts @@ -32,5 +32,26 @@ class StringWithTokens extends z.ZodString { } } +const actionInstanceShape = { + name: z.string(), + description: z.string(), + icon: z.string(), + action: z.string(), + actionInstanceId: z.string().uuid(), +}; + +export type ActionInstanceConfig = z.infer>; + +class ActionInstance extends z.ZodObject { + static create = () => + new ActionInstance({ + typeName: "ActionInstance" as z.ZodFirstPartyTypeKind.ZodObject, + shape: () => actionInstanceShape, + catchall: z.never(), + unknownKeys: "strip", + }); +} + export const markdown = Markdown.create; export const stringWithTokens = StringWithTokens.create; +export const actionInstance = ActionInstance.create; diff --git a/core/actions/api/index.ts b/core/actions/api/index.ts index 7fc3600ff..520fe18a9 100644 --- a/core/actions/api/index.ts +++ b/core/actions/api/index.ts @@ -2,9 +2,16 @@ import type * as z from "zod"; -import type { Event } from "db/public"; +import type { ActionInstances, Event } from "db/public"; -import { pubEnteredStage, pubInStageForDuration, pubLeftStage } from "../_lib/rules"; +import type { ReferentialRuleEvent } from "../types"; +import { + actionFailed, + actionSucceeded, + pubEnteredStage, + pubInStageForDuration, + pubLeftStage, +} from "../_lib/rules"; import * as datacite from "../datacite/action"; import * as email from "../email/action"; import * as googleDriveImport from "../googleDriveImport/action"; @@ -13,6 +20,7 @@ import * as log from "../log/action"; import * as move from "../move/action"; import * as pdf from "../pdf/action"; import * as pushToV6 from "../pushToV6/action"; +import { isReferentialRuleEvent, referentialRuleEvents } from "../types"; export const actions = { [log.action.name]: log.action, @@ -41,22 +49,33 @@ export const rules = { [pubInStageForDuration.event]: pubInStageForDuration, [pubEnteredStage.event]: pubEnteredStage, [pubLeftStage.event]: pubLeftStage, + [actionSucceeded.event]: actionSucceeded, + [actionFailed.event]: actionFailed, } as const; export const getRuleByName = (name: T) => { return rules[name]; }; +export const isReferentialRule = ( + rule: (typeof rules)[keyof typeof rules] +): rule is Extract => + referentialRuleEvents.includes(rule.event as any); + export const humanReadableEvent = ( event: T, config?: (typeof rules)[T]["additionalConfig"] extends undefined ? never - : z.infer> + : z.infer>, + watchedAction?: ActionInstances | null ) => { const rule = getRuleByName(event); if (config && rule.additionalConfig) { return rule.display.withConfig(config); } + if (watchedAction && isReferentialRule(rule)) { + return rule.display.withConfig(watchedAction); + } return rule.display.base; }; diff --git a/core/actions/types.ts b/core/actions/types.ts index cbc2758a4..117baad0d 100644 --- a/core/actions/types.ts +++ b/core/actions/types.ts @@ -3,16 +3,17 @@ import type * as z from "zod"; import type { ProcessedPub } from "contracts"; import type { + ActionInstances, Action as ActionName, ActionRunsId, CommunitiesId, - Event, PubsId, StagesId, } from "db/public"; import type { LastModifiedBy } from "db/types"; import type { Dependency, FieldConfig, FieldConfigItem } from "ui/auto-form"; import type * as Icons from "ui/icon"; +import { Event } from "db/public"; import type { CorePubField } from "./corePubFields"; import type { ClientExceptionOptions } from "~/lib/serverActions"; @@ -176,6 +177,12 @@ export const defineRun = ( export type Run = ReturnType; +export const referentialRuleEvents = [Event.actionSucceeded, Event.actionFailed] as const; +export type ReferentialRuleEvent = (typeof referentialRuleEvents)[number]; + +export const isReferentialRuleEvent = (event: Event): event is ReferentialRuleEvent => + referentialRuleEvents.includes(event as any); + export type EventRuleOptionsBase< E extends Event, AC extends Record | undefined = undefined, @@ -195,7 +202,13 @@ export type EventRuleOptionsBase< /** * The display name for this event when used in a rule */ - [K in "withConfig" as AC extends Record ? K : never]: (options: AC) => string; + [K in "withConfig" as AC extends Record + ? K + : E extends ReferentialRuleEvent + ? K + : never]: ( + options: AC extends Record ? AC : ActionInstances + ) => string; }; }; diff --git a/core/app/c/[communitySlug]/stages/manage/actions.ts b/core/app/c/[communitySlug]/stages/manage/actions.ts index 577ebd3ab..a8eda541f 100644 --- a/core/app/c/[communitySlug]/stages/manage/actions.ts +++ b/core/app/c/[communitySlug]/stages/manage/actions.ts @@ -33,7 +33,7 @@ import { revalidateTagsForCommunity } from "~/lib/server/cache/revalidate"; import { findCommunityBySlug } from "~/lib/server/community"; import { defineServerAction } from "~/lib/server/defineServerAction"; import { insertStageMember } from "~/lib/server/member"; -import { createRule, removeRule } from "~/lib/server/rules"; +import { createRule, createRuleWithCycleCheck, removeRule, RuleError } from "~/lib/server/rules"; import { createMoveConstraint as createMoveConstraintDb, createStage as createStageDb, @@ -390,22 +390,23 @@ export const addRule = defineServerAction(async function addRule({ } try { - await createRule({ + await createRuleWithCycleCheck({ actionInstanceId: data.actionInstanceId as ActionInstancesId, event: data.event, config: "additionalConfiguration" in data ? data.additionalConfiguration : null, - }).executeTakeFirstOrThrow(); + watchedActionId: + "watchedActionInstanceId" in data ? data.watchedActionInstanceId : undefined, + }); } catch (error) { logger.error(error); - if (error.message?.includes("unique constraint")) { + if (error instanceof RuleError) { return { - title: "Rule already exists", - error: `A rule for '${humanReadableEvent(data.event)}' and this action already exists. Please add another action - of the same type to this stage in order to have the same action trigger - multiple times for '${humanReadableEvent(data.event)}'.`, + title: "Error creating rule", + error: error.message, cause: error, }; } + console.log(error); return { error: "Failed to add rule", diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRule.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRule.tsx index bddaeab3e..65cbd18dc 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRule.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRule.tsx @@ -2,9 +2,10 @@ import { useCallback } from "react"; -import type { Action, CommunitiesId, Event, RulesId, StagesId } from "db/public"; +import type { Action, ActionInstances, CommunitiesId, Event, RulesId, StagesId } from "db/public"; import { Button } from "ui/button"; import { Trash } from "ui/icon"; +import { cn } from "utils"; import type { RuleForEvent } from "~/actions/_lib/rules"; import type { RuleConfig } from "~/actions/types"; @@ -18,15 +19,15 @@ type Props = { rule: { id: RulesId; event: Event; - instanceName: string; - action: Action; + actionInstance: ActionInstances; + watchedActionInstance?: ActionInstances | null; config?: RuleConfig> | null; }; }; -const actionIcon = (actionName: Action) => { - const action = getActionByName(actionName); - return ; +const ActionIcon = (props: { actionName: Action; className?: string }) => { + const action = getActionByName(props.actionName); + return ; }; export const StagePanelRule = (props: Props) => { @@ -40,15 +41,33 @@ export const StagePanelRule = (props: Props) => {
- {actionIcon(rule.action)} + If{" "} - {rule.instanceName} - {" "} - will run when{" "} - - {humanReadableEvent(rule.event, rule.config ?? undefined)} + {rule.watchedActionInstance ? ( + <> + + {humanReadableEvent( + rule.event, + rule.config ?? undefined, + rule.watchedActionInstance + )} + + ) : ( + humanReadableEvent(rule.event, rule.config ?? undefined) + )} + , run{" "} + + + {rule.actionInstance.name} + {" "}
diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRuleCreator.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRuleCreator.tsx index d56df722c..6ace1d3e5 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRuleCreator.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelRuleCreator.tsx @@ -1,5 +1,7 @@ "use client"; +import type { ControllerRenderProps, FieldValue, UseFormReturn } from "react-hook-form"; + import { useCallback, useState } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; @@ -12,7 +14,8 @@ import type { CommunitiesId, StagesId, } from "db/public"; -import { Event } from "db/public"; +import { actionInstancesIdSchema, Event } from "db/public"; +import { ActionInstanceProvider } from "ui/actionInstances"; import { AutoFormObject } from "ui/auto-form"; import { Button } from "ui/button"; import { @@ -27,9 +30,10 @@ import { import { Form, FormField, FormItem, FormLabel, FormMessage } from "ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select"; -import type { Rules } from "~/actions/_lib/rules"; +import type { RuleConfig, RuleForEvent, Rules } from "~/actions/_lib/rules"; +import type { ReferentialRuleEvent } from "~/actions/types"; import { actions, getRuleByName, humanReadableEvent, rules } from "~/actions/api"; -import { useServerAction } from "~/lib/serverActions"; +import { isClientException, useServerAction } from "~/lib/serverActions"; import { addRule } from "../../../actions"; type Props = { @@ -39,36 +43,132 @@ type Props = { rules: { id: string; event: Event; - instanceName: string; - action: Action; - actionInstanceId: ActionInstancesId; + actionInstance: ActionInstances; + watchedAction?: ActionInstances; + config?: RuleConfig> | null; }[]; }; -const schema = z.discriminatedUnion("event", [ - z.object({ - event: z.literal(Event.pubEnteredStage), - actionInstanceId: z.string().uuid(), - }), - z.object({ - event: z.literal(Event.pubLeftStage), - actionInstanceId: z.string().uuid(), - }), - ...Object.values(rules) - .filter( - (rule): rule is Exclude => - rule.event !== Event.pubEnteredStage && rule.event !== Event.pubLeftStage - ) - .map((rule) => - z.object({ - event: z.literal(rule.event), - actionInstanceId: z.string().uuid(), - additionalConfiguration: rule.additionalConfig - ? rule.additionalConfig - : z.null().optional(), - }) - ), -]); +// Action selector component to be reused +const ActionSelector = ({ + fieldProps, + actionInstances, + label, + placeholder, + disabledActionId, +}: { + fieldProps: Omit, "name">; + actionInstances: ActionInstances[]; + label: string; + placeholder: string; + disabledActionId?: string; +}) => { + return ( + + {label} + + + + ); +}; + +const schema = z + .discriminatedUnion("event", [ + z.object({ + event: z.literal(Event.pubEnteredStage), + actionInstanceId: actionInstancesIdSchema, + }), + z.object({ + event: z.literal(Event.pubLeftStage), + actionInstanceId: actionInstancesIdSchema, + }), + z.object({ + event: z.literal(Event.actionSucceeded), + actionInstanceId: actionInstancesIdSchema, + watchedActionInstanceId: actionInstancesIdSchema, + }), + z.object({ + event: z.literal(Event.actionFailed), + actionInstanceId: actionInstancesIdSchema, + watchedActionInstanceId: actionInstancesIdSchema, + }), + ...Object.values(rules) + .filter( + ( + rule + ): rule is Exclude< + Rules, + { + event: + | Event.pubEnteredStage + | Event.pubLeftStage + | Event.actionSucceeded + | Event.actionFailed; + } + > => + ![ + Event.pubEnteredStage, + Event.pubLeftStage, + Event.actionSucceeded, + Event.actionFailed, + ].includes(rule.event) + ) + .map((rule) => + z.object({ + event: z.literal(rule.event), + actionInstanceId: actionInstancesIdSchema, + additionalConfiguration: rule.additionalConfig + ? rule.additionalConfig + : z.null().optional(), + }) + ), + ]) + .superRefine((data, ctx) => { + if (data.event !== Event.actionSucceeded && data.event !== Event.actionFailed) { + return; + } + + if (data.watchedActionInstanceId === data.actionInstanceId) { + ctx.addIssue({ + path: ["watchedActionInstanceId"], + code: z.ZodIssueCode.custom, + message: "Rules may not trigger actions in a loop", + }); + } + }); export type CreateRuleSchema = z.infer; @@ -77,10 +177,12 @@ export const StagePanelRuleCreator = (props: Props) => { const [isOpen, setIsOpen] = useState(false); const onSubmit = useCallback( async (data: CreateRuleSchema) => { - setIsOpen(false); - runAddRule({ stageId: props.stageId, data }); + const result = await runAddRule({ stageId: props.stageId, data }); + if (!isClientException(result)) { + setIsOpen(false); + } }, - [props.communityId] + [props.stageId, runAddRule] ); const onOpenChange = useCallback((open: boolean) => { @@ -89,15 +191,41 @@ export const StagePanelRuleCreator = (props: Props) => { const form = useForm>({ resolver: zodResolver(schema), + defaultValues: { + actionInstanceId: undefined, + event: undefined, + }, }); const event = form.watch("event"); + const selectedActionInstanceId = form.watch("actionInstanceId"); + const watchedActionInstanceId = form.watch("watchedActionInstanceId"); + + // For action chaining events, filter out self-references + const isActionChainingEvent = event === Event.actionSucceeded || event === Event.actionFailed; + + // Get disallowed events based on current configuration + const getDisallowedEvents = useCallback(() => { + if (!selectedActionInstanceId) return []; - const selectedAction = form.watch("actionInstanceId"); + return props.rules + .filter((rule) => { + // For regular events, disallow if same action+event already exists + if (rule.event !== Event.actionSucceeded && rule.event !== Event.actionFailed) { + return rule.actionInstance.id === selectedActionInstanceId; + } - const disallowedEvents = props.rules - .filter((rule) => rule.actionInstanceId === selectedAction) - .map((rule) => rule.event); + // For action chaining events, allow multiple rules with different watched actions + return ( + rule.actionInstance.id === selectedActionInstanceId && + rule.event === event && + rule.watchedAction?.id === watchedActionInstanceId + ); + }) + .map((rule) => rule.event); + }, [props.rules, selectedActionInstanceId, event, form]); + + const disallowedEvents = getDisallowedEvents(); const allowedEvents = Object.values(Event).filter((event) => !disallowedEvents.includes(event)); const rule = getRuleByName(event); @@ -125,36 +253,12 @@ export const StagePanelRuleCreator = (props: Props) => { control={form.control} name="actionInstanceId" render={({ field }) => ( - - - - - + )} /> { name="event" render={({ field }) => ( - + when... {allowedEvents.length > 0 ? ( <> + + + + + {actionInstances.map((instance) => { + const action = actions[instance.action]; + + return ( + +
+ + {instance.name} +
+
+ ); + })} +
+ + +
+ )} + /> + ); +}; diff --git a/packages/ui/src/actionInstances/ActionInstanceSelectorInput.tsx b/packages/ui/src/actionInstances/ActionInstanceSelectorInput.tsx new file mode 100644 index 000000000..a83cbf556 --- /dev/null +++ b/packages/ui/src/actionInstances/ActionInstanceSelectorInput.tsx @@ -0,0 +1,8 @@ +import React from "react"; + +import type { AutoFormInputComponentProps } from "../auto-form"; +import { ActionInstanceSelector } from "./ActionInstanceSelector"; + +export const ActionInstanceSelectorInput = (props: AutoFormInputComponentProps) => { + return ; +}; diff --git a/packages/ui/src/actionInstances/ActionInstancesContext.tsx b/packages/ui/src/actionInstances/ActionInstancesContext.tsx new file mode 100644 index 000000000..4d664176a --- /dev/null +++ b/packages/ui/src/actionInstances/ActionInstancesContext.tsx @@ -0,0 +1,48 @@ +"use client"; + +import type { JSX } from "react"; + +import React, { createContext, useContext } from "react"; + +import type { Action, ActionInstances } from "db/public"; + +type ActionType = { + name: Action; + config: { + schema: Record; + }; + description: string; + params: { + schema: Record; + }; + icon: (props: any) => React.ReactNode | JSX.Element; + superAdminOnly?: boolean; + experimental?: boolean; + tokens?: Record; +}; + +export type ActionInstanceContext = { + actions: Record; + actionInstances: ActionInstances[]; +}; + +type Props = { + children: React.ReactNode; + actionInstances: ActionInstances[]; + actions: Record; +}; + +const ActionInstanceContext = createContext({ + actions: {}, + actionInstances: [], +}); + +export function ActionInstanceProvider({ children, actionInstances, actions }: Props) { + return ( + + {children} + + ); +} + +export const useActionInstanceContext = () => useContext(ActionInstanceContext); diff --git a/packages/ui/src/actionInstances/index.tsx b/packages/ui/src/actionInstances/index.tsx new file mode 100644 index 000000000..364bd3106 --- /dev/null +++ b/packages/ui/src/actionInstances/index.tsx @@ -0,0 +1,2 @@ +export * from "./ActionInstancesContext"; +export * from "./ActionInstanceSelector"; diff --git a/packages/ui/src/auto-form/config.ts b/packages/ui/src/auto-form/config.ts index 613c0f4f3..bc76308ea 100644 --- a/packages/ui/src/auto-form/config.ts +++ b/packages/ui/src/auto-form/config.ts @@ -1,3 +1,4 @@ +import { ActionInstanceSelectorInput } from "../actionInstances/ActionInstanceSelectorInput"; import { InputWithTokens, MarkdownEditor } from "../editors"; import AutoFormCheckbox from "./fields/checkbox"; import AutoFormDate from "./fields/date"; @@ -21,6 +22,7 @@ export const INPUT_COMPONENTS = { fallback: AutoFormInput, markdown: MarkdownEditor, stringWithTokens: InputWithTokens, + actionInstance: ActionInstanceSelectorInput, }; /** @@ -37,4 +39,5 @@ export const DEFAULT_ZOD_HANDLERS: { ZodNumber: "number", Markdown: "markdown", StringWithTokens: "stringWithTokens", + ActionInstance: "actionInstance", };