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.
Action base classes
Section titled “Action base classes”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.
Declaring the input form
Section titled “Declaring the input form”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 withString,Text,Bool,Integer,Decimal,DateOnly,DateTime,TimeOnly, orSelectplusOption(key, label), andRequired,Hint,Sensitive.EntityPicker<TEntity>,UserPicker,RolePicker, andResponsibilityGroupPickerbind 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.
Defaults, nullability, and Select values
Section titled “Defaults, nullability, and Select values”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.
Minimal example
Section titled “Minimal example”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.")); }}Placing actions on entity surfaces
Section titled “Placing actions on entity surfaces”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-modelGET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/actionsPOST /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/actionsPUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/actions/{actionId}DELETE /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/actions/{actionId}Runtime flow
Section titled “Runtime flow”Clients discover and run actions through the runtime API:
GET /api/workspace/entity/{entityIdOrKey}/ui/actionsGET /api/workspace/entity/{entityIdOrKey}/ui/actions/{actionId}GET /api/workspace/entity/{entityIdOrKey}/ui/actions/activePOST /api/workspace/entity/{entityIdOrKey}/ui/actions/{actionId}/validatePOST /api/workspace/entity/{entityIdOrKey}/ui/actions/{actionId}/enqueue- The list endpoint takes a
Surfacequery value (Drawer,DetailsPage, orEntityListPage) and returns the actions available to the caller on that surface; the single-action endpoint adds the full input metadata. activereturns queued or running executions for runtime UI state, filterable byActionId,EntityInstanceId, andSelectedEntityInstanceIdsquery values.validateruns schema validation andOnValidateInputAsyncsynchronously and returns the result without queueing anything.enqueuevalidates the same way, then queues the run and returns the job with itsJobId. 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.