Actions
Actions represent the operations a user can perform on a record - approving a task, closing a ticket, assigning a deal. Each action triggers a state change, runs server-side logic, or both. Actions are defined in actions/index.ts and exported as an AppAction[] array.
Base actions
Every app inherits a set of core actions from the @comind/base plugin. You do not need to redeclare them:
add- create a new recordedit- modify an existing recorddelete- remove a recordcopy- duplicate a recordmove- move a record to another workspacearchive- archive a record
Apps add their own actions on top of these. A CRM deal might add qualify, negotiate, and close-won. A support ticket might add escalate, resolve, and reopen. See Plugins for how base actions merge with app-specific ones.
Defining actions
Actions are defined in actions/index.ts and typed as AppAction[]. Each entry describes one operation the user can invoke:
import { AppAction } from '@comind/api';
export const actions: AppAction[] = [
{
name: 'approve',
caption: 'Approve',
from: 'review',
to: 'approved',
},
{
name: 'reject',
caption: 'Reject',
from: 'review',
to: 'rejected',
},
];
The from and to properties tie the action to state values on the record's state field. When the user clicks "Approve", the record's state moves from review to approved. This structure is what makes actions appear as buttons in the UI only when the record is in the correct state.
You can split definitions across multiple files and re-export them from actions/index.ts, the same way Fields are organized into topical modules. The engine only cares about the final exported AppAction[].
Action logic
Server-side code for actions lives in actions/logic/index.ts, exported as a default object typed with satisfies ActionLogic<typeof entity>. This code runs on the C# backend via Jint (a JavaScript interpreter for .NET) - not in the browser.
import { entity } from '#typings';
import type { ActionLogic } from '@comind/api';
export default {
approve,
reject,
} satisfies ActionLogic<typeof entity>;
function approve() {
entity.c_approved_date = new Date().toISOString();
entity.c_approved_by = currentUser.id;
}
function reject() {
entity.c_rejection_reason = entity.c_rejection_reason || 'No reason provided';
}
Each method name matches an action's name. When that action fires, the engine calls the matching method. Inside the method you can read and write fields on entity, compare against entityOld, query other records, call external APIs, or generate files.
Do not use as const satisfies ActionLogic - omit as const. Do not import makeActionLogic - the builder auto-injects it during compilation.
Available globals
Action logic methods have access to several global objects. The most commonly used are:
entity- current record being processed (mutable)entityOld- previous state of the record (read-only)records- array of records when running bulk operationscurrentUser- the user executing the action, with methods for role checks (isGlobalAdmin,isWorkspaceAdmin) and group membership (isInGroup(),isInAnyOfGroups())ComindServer- the main server API for querying, creating, updating, and deleting records, plus ACL helpers and utility methodsRestApi- HTTP client for calling external APIsFiles- file operations for Excel, CSV, PDF, and ZIPAppSchema- install or uninstall apps, access variables and secretsUtils- formatting, date math, GUID generation, hashingconsole- logging withlog,debug,warn,info, anderror
For the full method reference on each global, see Server-side API.
Preconditions
Precondition checks live in actions/logic/preconditions.ts, exported as a default object typed with satisfies PreconditionLogic<typeof entity>. They control when an action is available to a user. If a precondition returns false, the action button is hidden or disabled in the UI.
import { entity } from '#typings';
import type { PreconditionLogic } from '@comind/api';
export default {
approve: () => currentUser.isInGroup('Reviewers') && entity.state === 'review',
delete: () => currentUser.isWorkspaceAdmin || entity.created_by === currentUser.id,
} satisfies PreconditionLogic<typeof entity>;
Use preconditions to enforce business rules based on state, user role, field values, or group membership. The method name matches the action it guards. You can override preconditions for base actions like delete to add stricter access control.
Do not import makePreconditionLogic - the builder auto-injects it during compilation.
Bulk operations
When an action runs on multiple records at once (mass edit), the records array is populated instead of entity. Your action logic can iterate over all affected records:
function bulk_approve() {
for (const record of records) {
record.c_approved_date = new Date().toISOString();
}
}
Impersonation
The ComindServer API supports a runAsUser parameter on createAppRecord(), updateAppRecord(), and deleteAppRecord(). This lets action logic create or modify records as a different user - useful for automated workflows where the system needs to act on someone's behalf.
ComindServer.createAppRecord('TASK', { title: 'Follow-up' }, targetUserId);
Customizing actions through settings
Actions can be adjusted through app settings without changing code. The Settings model supports overriding:
- Action caption (the label shown on the button)
- Action visibility (show or hide specific actions)
- Help text displayed alongside the action
- Group names used in precondition checks
This lets workspace administrators fine-tune the UI for their team without modifying the app package.
Plugin logic combination
When multiple plugins define ActionLogic or PreconditionLogic, their methods are combined during composition. Each plugin's logic runs in sequence - the base plugin's logic executes first, then each additional plugin's logic in dependency order, and finally the app's own logic. This means a plugin can set up default behavior that the app layer refines or overrides.
For details on where action files sit within the app directory, see Package structure.