Skip to content

Commands and API functions

Commands and API functions expose C# business logic to direct API callers — integrations, AI agents, and the product UI. Both are authored in the workspace Net Operation Project with the Moltaro .NET SDK programming model, in the Constructor area under group Automation & logic > Development (page “Business Logic Development”, NET Project mode). Two shapes exist:

  • Commands run synchronously: the caller waits and receives the typed result in the HTTP response.
  • API functions are global functions enqueued as background jobs: the caller gets a job id and polls its status.

Do not decide between them from an async method or HTTP status. Read Kind from GET /api/workspace/admin/api-functions (Command or Enqueue) and the active Function Catalog contract. Moltaro’s enqueue operation returns HTTP 200 with a queued status model; completion is established only by polling that model to a terminal state. See Asynchronous operations and polling.

A command derives from CommandFunction<TArgs, TResult> (or CommandFunction<TResult> when it takes no arguments). TArgs and TResult are JSON-serializable types; the runtime deserializes the request Args into TArgs before the command runs. The command returns one of three outcomes:

  • CommandResult<TResult>.Ok(result, message) — success with a typed payload;
  • CommandResult<TResult>.Failed(message) — a business failure;
  • CommandResult<TResult>.ValidationFailed(issues) — rejection with field-level validation issues.

Two attributes declare and publish the command:

  • [MoltaroFunction(id)] assigns the durable function id. The id is also the stable callable key unless an explicit Key is set; optional Name and Description are shown to administrators and configurators.
  • [MoltaroCommand] publishes the function on the commands API. Access is fail-closed: exactly one of PermissionKey or AllowAnyAuthenticatedUser = true must be configured, and publications without an access declaration are rejected at build and apply time. Anonymous invocation is never supported. Optional TimeoutSeconds (1–86400) overrides the synchronous execution timeout; the effective timeout never exceeds the runtime action timeout setting.

PermissionKey is a lowercase dotted identifier such as acme.commands.recalculate-price. It is surfaced in the roles administration catalog, so administrators grant the command to roles like any other permission.

using Moltaro.Package.NET.Functions;
public sealed record RecalculatePriceArgs(string ProductCode);
public sealed record RecalculatePriceResult(decimal NewPrice);
[MoltaroFunction("acme-recalculate-price")]
[MoltaroCommand(PermissionKey = "acme.commands.recalculate-price")]
public sealed class RecalculatePriceCommand
: CommandFunction<RecalculatePriceArgs, RecalculatePriceResult>
{
public override async Task<CommandResult<RecalculatePriceResult>> OnRunAsync(
RecalculatePriceArgs args,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(args.ProductCode))
{
return CommandResult<RecalculatePriceResult>.ValidationFailed(
new ValidationIssue("Product code is required.", "ProductCode"));
}
var newPrice = await RecalculateAsync(args.ProductCode, cancellationToken);
return CommandResult<RecalculatePriceResult>.Ok(
new RecalculatePriceResult(newPrice), "Price recalculated.");
}
}

POST /api/workspace/commands/{functionKey} with a bearer token (see the integration quickstart for authentication). The body is optional; both properties may be omitted:

{ "Args": { "ProductCode": "SKU-100" }, "CorrelationId": "order-4711" }

A successful invocation returns the standard envelope with the run result:

{
"Data": {
"RunId": "d3f1…",
"Message": "Price recalculated.",
"Result": { "NewPrice": 129.9 },
"DurationMs": 42,
"CorrelationId": "order-4711"
},
"Errors": [],
"Warnings": [],
"Success": true
}

The error contract:

  • 400 with field-level errors when the command returned ValidationFailed — each issue’s field key appears as the error Field;
  • 400 with a business error carrying the message from Failed;
  • 403 when the caller is authenticated but lacks the permission;
  • 404 for unknown, disabled, and retired keys alike — they are indistinguishable, so callers cannot probe which commands exist.

[MoltaroApiEnqueue] publishes a job-contract global function for asynchronous enqueue. The same fail-closed access rule applies: exactly one of PermissionKey or AllowAnyAuthenticatedUser, never anonymous. Callers with access can also observe and cancel the resulting queued jobs. Optional TimeoutSeconds accepts 1–86400 seconds and is captured on each queued job, so all retries keep the same timeout. When it is omitted, the runtime Job timeout setting applies.

[MoltaroFunction("acme-rebuild-products")]
[MoltaroApiEnqueue(
PermissionKey = "acme.jobs.rebuild-products",
TimeoutSeconds = 18000)]
public sealed class RebuildProductsJob : GlobalFunction
{
public override Task<FunctionResult> OnRunAsync(
CancellationToken cancellationToken)
{
return Task.FromResult(FunctionResult.Ok("Product rebuild completed."));
}
}

POST /api/workspace/functions/{functionKey}/enqueue accepts optional queue options and returns the queued job status:

{
"Args": { "ProductCode": "SKU-100" },
"DeduplicationKey": "rebuild-SKU-100",
"RunAfter": "2026-07-22T18:00:00Z",
"CorrelationId": "order-4711"
}

DeduplicationKey reuses an active queued job instead of enqueueing a duplicate; RunAfter sets the earliest execution timestamp. Unknown and disabled function keys answer 404, exactly like commands.

  • GET /api/workspace/functions/jobs/{jobId} returns the job status — status, timestamps, attempt count, captured TimeoutSeconds, CanCancel, LastError — plus the latest execution with its result payload and error summary.
  • POST /api/workspace/functions/jobs/{jobId}/cancel cancels a job that is still queued; jobs already claimed by a worker can no longer be cancelled.

A job moves through Queued (waiting for a worker), Leased (claimed by a worker under an active lease), and ends as Completed, Failed, or Cancelled. A job is visible to its initiator and to every holder of the publication permission. Run history and diagnostics for all function executions live in Operations.

Workspace UI pages use the authenticated MoltaroApiClient instead of manually supplying bearer or locale headers. It exposes typed command and job helpers, AbortSignal support, explicit numeric job/run status enums, and a bounded polling helper:

import {
MoltaroApiClient,
WorkspaceUiFunctionJobStatusEnum,
} from '@moltaro/workspace-ui'
interface RebuildResult {
RebuiltCount: number
}
const api = new MoltaroApiClient()
const queued = await api.enqueueFunction<RebuildResult>('rebuild-report', {
args: { Month: '2026-07' },
deduplicationKey: 'rebuild-report-2026-07',
})
const finished = await api.waitForFunctionJob<RebuildResult>(queued.JobId, {
timeoutMs: 120_000,
})
if (finished.Status === WorkspaceUiFunctionJobStatusEnum.Completed) {
console.log(finished.Run?.ResultData?.RebuiltCount)
}

The page always runs under the signed-in user’s permissions. A function job ending as Failed or Cancelled is a successful status read rather than an HTTP exception; inspect its status, LastError, and latest run. Transport, authorization, and validation errors keep the standard API error semantics. See the complete Workspace UI Moltaro API client contract.

Every command and enqueue publication is listed on the API Functions page in the Administration area, group Monitoring, with an enable/disable switch per publication. Disabling a publication makes its key answer 404 immediately, without touching the function source. The same surface is available as an admin API under /api/workspace/admin/api-functions: list publications, read one, and update settings with PUT /api/workspace/admin/api-functions/{publicationId}/settings sending IsEnabled plus the publication RowVersion for optimistic concurrency. Publication changes are audit-logged.