Skip to content

Actions

Actions are C# functions that users trigger from entity screens. An action declares a typed input model, an optional input form, and a run method; the platform renders the form, validates the input, queues the run, and records the execution. Actions can be authored in the workspace Net Operation Project or in a trusted package using the Moltaro .NET SDK programming model. Workspace-specific Actions are developed in the Constructor area, group “Automation & logic”, on the Development page (titled “Business Logic Development”) in NET Project mode. Administrators and designers own the UI bindings that place either kind of Action on runtime surfaces.

Each action derives from one base class that fixes its target scope:

  • GlobalActionFunction<TInput> — no entity target; OnRunAsync(input, cancellationToken).
  • EntityInstanceActionFunction<TEntity, TInput> — targets one record; OnRunAsync(entityInstanceId, input, cancellationToken).
  • EntitySelectionActionFunction<TEntity, TInput> — targets the records the user selected; OnRunAsync(entityInstanceIds, input, cancellationToken).
  • EntityCollectionActionFunction<TEntity, TInput> — targets the entity collection as a whole; OnRunAsync(input, cancellationToken).

Selection and collection actions on the Data Explorer list are desktop-only. The narrow/mobile Data Explorer intentionally exposes no selection controls or group/query-wide actions; switch to desktop to run these actions. Ordinary row operations such as opening details or editing a concrete record remain available from that record’s mobile card; this is not an EntityInstance Business Function placement on EntityListPage.

TEntity is the entity class generated for the record type. Every base class has one abstract OnRunAsync returning a FunctionResult (Ok, OkData, Skipped, Failed) and one virtual OnValidateInputAsync returning a ValidationResult. Input validation runs after the platform has checked the declared input schema (required flags, lengths, bounds) and before the run is queued; return ValidationResult.Valid to allow queueing, or ValidationResult.Invalid(...) with issues to block it. Actions that collect no user input use the NoActionInput marker record as TInput.

The input model is a plain C# type. Attach an input configuration with [ActionInputConfiguration(typeof(...))] and describe fields, form layout, and the history card in one place:

  • Field(x => x.Prop, "Label") adds a scalar field; refine it with String, Text, Bool, Integer, Decimal, DateOnly, DateTime, TimeOnly, or Select plus Option(key, label), and Required, Hint, Sensitive.
  • EntityPicker<TEntity>, UserPicker, RolePicker, and ResponsibilityGroupPicker bind pickers to string id properties.
  • Form() defines the dialog layout: Name, MaxWidth, ColumnCount, plus field, text, and alert items with column placement and editor preferences.
  • Card() defines the history card that displays the submitted input on execution records, with optional tabs.

If the configuration declares no fields, one field is inferred per public property: the CLR type maps to a field type (enums become selects), [Required] and non-nullable value types mark fields required, and [StringLength], [MaxLength], and [Display] are honored.

Action-input defaults are versioned schema metadata. A new non-nullable Bool field has the implicit default false; use .Default(true) or .Default(false) to declare a specific Boolean default. A nullable bool? has no implicit default, but may declare one with .Default(true) or .Default(false). For Select fields, use .DefaultOption("key") for a scalar value and .Select(allowMultiple: true).DefaultOptions("key-a", "key-b") for a multi-select. Every key must be one of the field’s declared Option keys.

The form sends configured defaults even when the user does not touch the control. Fields without a schema default stay absent until the user changes them. This distinction is intentional: an absent property lets an ordinary CLR property initializer run during typed deserialization, while explicit null means the caller cleared the field and is validated as such. A CLR property initializer is never executed during discovery and does not create a visible UI default; declare visible defaults through the SDK.

Each configured input field must use a public CLR property that binds through a public set/init, an explicit JsonInclude setter, or a compatible JsonConstructor parameter. Moltaro owns the action-input wire property name, so do not rename or suppress configured properties with JsonPropertyName or read-suppressing JsonIgnore; ambiguous, renamed, ignored, or non-bindable read-only properties are rejected during discovery.

Defaults are not allowed on sensitive inputs. .Required() may be combined with a default: the form starts with the configured value, and validation rejects clearing it.

using System.Threading;
using System.Threading.Tasks;
using Moltaro.Package.NET.Functions;
using Moltaro.Operational.Generated.Entities;
public sealed record EscalateInput
{
public string? AssigneeUserId { get; init; }
public string Reason { get; init; } = string.Empty;
}
public sealed class EscalateInputConfiguration : ActionInputConfiguration<EscalateInput>
{
protected override void Configure(ActionInputBuilder<EscalateInput> input)
{
input.UserPicker(x => x.AssigneeUserId, "Assignee").Required();
input.Field(x => x.Reason, "Reason").Text(maxLength: 2000).Required();
input.Form().Name("Escalate ticket").ColumnCount(1);
}
}
[MoltaroFunction("acme.supportTicket.escalate", Name = "Escalate ticket")]
[ActionInputConfiguration(typeof(EscalateInputConfiguration))]
public sealed class EscalateTicketAction
: EntityInstanceActionFunction<SupportTicket, EscalateInput>
{
public override Task<FunctionResult> OnRunAsync(
string entityInstanceId,
EscalateInput input,
CancellationToken cancellationToken)
{
// Reassign the ticket and record the escalation here.
return Task.FromResult(FunctionResult.Ok("Ticket escalated."));
}
}

Actions appear on three entity UI surfaces: the record drawer, the details page, and the entity list page. Bindings are managed per entity definition through the action designer, which offers only functions that are published with the Action contract, enabled, entity-scoped, and owned by that entity definition. A binding sets the surface, the target scope (EntityInstance, EntitySelection, or EntityCollection), toolbar placement with Primary or Overflow presentation, and the button color and variant. Managing bindings requires definition-management access. On EntityListPage, bindings with EntitySelection or EntityCollection scope are not rendered in the narrow/mobile Data Explorer:

GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/actions/designer-model
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/actions
POST /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/actions
PUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/actions/{actionId}
DELETE /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/actions/{actionId}

Clients discover and run actions through the runtime API:

GET /api/workspace/entity/{entityIdOrKey}/ui/actions
GET /api/workspace/entity/{entityIdOrKey}/ui/actions/{actionId}
GET /api/workspace/entity/{entityIdOrKey}/ui/actions/active
POST /api/workspace/entity/{entityIdOrKey}/ui/actions/{actionId}/validate
POST /api/workspace/entity/{entityIdOrKey}/ui/actions/{actionId}/enqueue
  • The list endpoint takes a Surface query value (Drawer, DetailsPage, or EntityListPage) and returns the actions available to the caller on that surface; the single-action endpoint adds the full input metadata.
  • active returns queued or running executions for runtime UI state, filterable by ActionId, EntityInstanceId, and SelectedEntityInstanceIds query values.
  • validate runs schema validation and OnValidateInputAsync synchronously and returns the result without queueing anything.
  • enqueue validates the same way, then queues the run and returns the job with its JobId. Execution is asynchronous: the function runs in the background, and every enqueue is audit-logged with the action id, function id, and target scope.

The validate response is terminal for validation only; it does not mean that the action executed. After enqueue, use the returned job, the active endpoint, and Function operations to observe completion. The common job/run state model is explained in Asynchronous operations and polling.

The invocation body carries ActionTargetScope (it must match the binding), EntityInstanceId for instance scope, SelectedEntityInstanceIds for selection scope, the Input payload, and an optional CorrelationId. Track finished runs on the “Function operations” page (Administration area, group “Monitoring”) — see Operations and diagnostics.