Skip to content

.NET SDK for C# business logic

The Moltaro .NET SDK is the programming model for C# business logic in the workspace Net Operation Project: a function is a C# class that derives from an SDK base class and declares identity, bindings, and publication with attributes. Authoring happens in the Constructor area under Automation & logic > Development, or entirely through the Configuration API. AI agents should use the API-first authoring flow; downloading the NET Project ZIP is optional for a human using a local IDE.

Use the curated .NET reference together with the installation’s GET .../net-operation-project/developer-surface catalog. The catalog, not arbitrary public CLR visibility, defines supported assemblies and injectable services.

For durable publication, related Board/Workflow facts and the versioned ContextChanged contract, see Resource events.

For Workflow, use its configuration automation and runtime services. Runtime commands support Action, Command, HttpEndpoint, Job and TriggerHandler; configuration automation supports the first three. The process document reference describes the Engine types, version-2 YAML and durable typed observations.

Community Memberships exposes community commands and reads through ICommunityCommandService and ICommunityQueryService. Its Workspace payment-method catalog provides IPaymentMethodQueryService for list/point reads and IPaymentMethodCommandService.CreateAsync for creating enabled methods. The latter takes CreatePaymentMethodRequest with a display name, optional description and a stable OperationId; an unchanged retry returns the original ID and RowVersion. Both command services return MembershipCommandResult with ObjectId, DisplayName and RowVersion. The optional request CorrelationId links diagnostics; OperationId identifies an operation for exact retries.

These services share one scoped IActorContextProvider. An empty actor means trusted code and writes system attribution; set ActorUserId to a real user ID to check that user’s module permissions. Payment-method reads require Workspace payment-method.read; creation requires Workspace payment-method.manage and an enabled module. A new command transaction uses ReadCommitted; an existing caller transaction retains its isolation through a savepoint. Creation writes typed history and a command receipt atomically. The catalog classifies recorded payments; it configures no payment provider. Membership issuance, corporate groups and payment posting are not yet supported commands, and administrative module status continues to report RuntimeAvailable=false.

Moltaro.Package.NET.Formatting.MoltaroDateTimeFormatter provides common server-side display formatting for DateOnly, TimeOnly, DateTime and DateTimeOffset. Its constructor takes a CultureInfo, a TimeZoneInfo and the resolved .NET date, time and date-time format strings. Use the workspace formats/time zone and choose the language for the output’s audience; stored shared text uses workspace language. Formats use .NET tokens with literal / and : separators, keeping the workspace pattern’s punctuation when language changes. The formatter normalizes separators in its own read-only culture copy and leaves the supplied culture unchanged.

Call the typed Format(value) overload. Calendar dates and wall-clock times are not shifted. Instants are converted to the configured zone. A DateTime with Kind.Unspecified is wall time in that zone; a DST gap or overlap throws ArgumentException, so supply a UTC DateTime or DateTimeOffset when the instant matters. Kind.Local follows .NET’s source-machine local-zone semantics. The formatter does not parse strings, read ambient culture, or append a zone label by itself.

Existing presentation placeholders use this formatting automatically. No formatting function needs to be added to a template.

For governed Entity commands, SecurityMutationError.Metadata preserves named decision details from the server. The mutation receipt journal has no receipt-count limit. See reliable automation for the seven-day replay window and recovery rules. ThrowIfFailed() retains these details in MoltaroSdkException. Aggregate SafeFailure items carry only the stable code and public field; they exclude resource identities and arbitrary diagnostics.

Every Net Operation Project artifact records the gateway contract of the SDK it was built with, and the host runs only artifacts of its own contract. The check is exact equality: after an upgrade that changes a contract, the host refuses artifacts built for the older one, and an older host refuses artifacts built for a newer one. Nothing falls back or is skipped silently. Until the artifact is rebuilt, its function runs, commands, HTTP endpoints, Action launches and data sources fail, its event consumers stop, and Entity saves with its C# Validation or BeforeSaveMutation bindings fail.

The current SDK uses Net Operation Project gateway contract 13. SDK 4 moves the shared DSL to a separate assembly, so generated projects must rebuild their module dependencies as well as their own source. A full moltaroctl upgrade rebuilds and activates the working revision of the Net Operation Project, so an unpublished draft in that revision becomes the active version; a manual Build replaces the active artifact the same way. Publish or discard unpublished changes before upgrading. Delta updates do not rebuild the project: for an upgrade that changes the gateway contract, arrange a full upgrade with the administrator. Work queued or leased with the old artifact can fail after its retries; the release notes of a version that changes a contract list the affected work and the order of the steps.

Derive a provider from DataSource<TParameters> and declare its stable identity, default name and description with [MoltaroDataSource]. Workspace-specific providers belong under src/DataSources/ in the Net Operation Project. Describe typed parameters, output schema, renderers and execution limits in Describe; keep data selection in ExecuteAsync and parameter choices in GetOptionsAsync.

SDK 6 removes RequirePermission, MinimumRole and the execution-requirements envelope. Technical module dependencies remain supported. After publishing, an Owner, Administrator or Configurator assigns Use source to users or roles in Data Sources administration. Unconfigured sources deny use, including to administrators. Catalog text overrides and categories need no rebuild, and Report/Dashboard visibility remains a separate setting. Source-use grants do not replace business-record authorization inside the provider.

SDK 6.1 lets category and time series sources name a series per row and a share of each value. Describe the fields with their display names and units; the builder assigns their roles:

builder.OutputCategorySeries(
new DataSourceFieldDescriptor("Type", DataSourceValueTypeEnum.String, DataSourceText.FromString("Task type")),
new DataSourceFieldDescriptor("Hours", DataSourceValueTypeEnum.Number, DataSourceText.FromString("Hours"), Unit: "h"))
.Series(
new("State", DataSourceValueTypeEnum.String, DataSourceText.FromString("State key")),
new("StateLabel", DataSourceValueTypeEnum.String, DataSourceText.FromString("State")))
.Share(new("Share", DataSourceValueTypeEnum.Percentage, DataSourceText.FromString("Share")))
.Renderer(DataSourceRendererKindEnum.Table);

Return one row per category (or time bucket) and series, sorted the way viewers should read them; series appear in the order of their first row and take theme colors in that order, so return every series in a fixed order, including zero rows. Category names must be unique unless you declare CategoryKey(new("WorkerId", DataSourceValueTypeEnum.String, ...)): then two categories with the same name, such as two Workers, stay apart. Leave an unknown value null rather than 0. A share is a fraction from 0 to 1 that your source calculates, with a denominator it chooses; reports show it in the share (100 %) bar view next to the absolute value and never recalculate it. Emit daily buckets as the local midnight of the day in the execution context’s TimeZoneId, with that day’s offset. Sources with a series are not offered as donut charts. Write new DataSourceFieldDescriptor(...) for the two output fields: a target-typed new(...) is ambiguous with the key overloads.

A table or record list row can open a Task in its preview drawer. Declare the visible field and the field holding the Task id, and name the visible one in a drilldown; the Task id field is never shown as a column:

builder.OutputTablePage(
new DataSourceFieldDescriptor("Task", DataSourceValueTypeEnum.String, DataSourceText.FromString("Task"), LinkRole: "TaskId"),
new DataSourceFieldDescriptor("TaskId", DataSourceValueTypeEnum.String, DataSourceText.FromString("Task id")))
.Drilldown(DataSourceLinkTargetKindEnum.Task, "Task");

A drilldown field without a link role, or whose link role names no declared field, links by its own value. A row whose Task id is null shows plain text. Opening a Task checks the viewer’s Task access again; a report never grants access to a Task.

Rebuild and republish older artifacts with SDK 6 (Package gateway 16 and Net Operation Project gateway 13). SDK 6.1 keeps both gateway contracts. The host keeps their saved catalog settings as unavailable until a compatible provider with the same identity is published; it does not convert old C# access declarations into grants.

Package and Net Operation Project assemblies execute in collectible runtime epochs. Keep work inside the invocation: use ordinary awaited async calls, and use Moltaro schedules or IFunctionQueue when work must run later. Check and Build reject detached, started, discarded, or unobserved stored tasks, threads and ThreadPool work (MOLTARO040), long-lived timers or callbacks (MOLTARO041), subscriptions to host events (MOLTARO042), and explicit default-context/dynamic-load or GCHandle escapes (MOLTARO044). CancellationToken.Register and UnsafeRegister are supported only when the returned registration itself is disposed by using. A local method-group delegate to either registration API is allowed only when every invocation is disposed this way and the delegate does not escape. Process-global ActivitySource.AddActivityListener registration and access to DiagnosticListener.AllListeners are rejected. Creating, subscribing to, or deriving from DiagnosticListener, and deriving from EventListener, are also rejected because their process-global registration cannot be made fail-closed in package code. Call MeterListener.Start only on a receiver declared by using var or a using declaration. A method-group delegate to Start must remain local and be invoked directly. A stored task must be directly awaited or returned by the very next statement; for parallel work, store a combined Task.WhenAll(...) task and await it next.

MOLTARO043 warns on every user-authored non-const static/static-readonly field. Review it as epoch-owned state: it must not escape to host state or retain callbacks, host services, collectible CLR types, delegates, or invocation values. Generated code and constants are excluded. Task.Delay and ordinary async/await are supported. An already active artifact is not stopped by a newly introduced analyzer rule, but its next Check or Build must pass the current contract.

Reflective host-event subscriptions, AppDomain.SetData, AppContext.SetData, process-principal replacement, named/thread-local data slots, and writes to external static fields or properties are lifetime escapes and are rejected as well. Process-global Encoding.RegisterProvider registration is also rejected. The same applies to TypeDescriptor.AddProvider, AddProviderTransparent, AddAttributes, AddEditorTable, and CreateAssociation. The same rule rejects process-global Console.SetIn/SetOut/SetError replacement and all Trace.Listeners collection access because those APIs can retain package-owned readers, writers, or listeners after the runtime epoch. Taking a method-group delegate to a forbidden process-global API is rejected too. Structured result helpers for command and function data, typed HTTP JSON, and data sources use the current runtime epoch’s serializer context automatically. Existing installed artifacts that explicitly pass process-singleton JSON options to those helpers remain runtime-safe because the epoch context replaces them; their next Check/Build rejects that reference with MOLTARO044, so omit the option. Direct JsonSerializer calls must pass a freshly constructed inline options instance or explicit typed JSON metadata; options obtained through parameters, fields, properties, or locals are rejected because their ownership cannot be proven. A JsonSerializer method group is allowed only when its selected overload requires JsonTypeInfo or JsonSerializerContext. Do not put serializer options, converters, runtime types, exceptions, or result objects into static or other process-global storage. Dynamic invocation is rejected because Check/Build cannot verify that the runtime-selected target respects these lifetime rules. Inline Activator.CreateInstance<T>() and CreateInstance(typeof(T)) remain available for ordinary non-listener types. Type-erased activation and Activator.CreateInstance method groups are rejected because the constructed lifetime cannot be verified.

Parent assignment and reparenting use the same supported record write services and optimistic concurrency as any Reference. See the Parent Tree View developer guide.

Reference Eligibility is likewise configured on Entity Definition fields or through the Configuration API, not authored from public Net Operation Project schema code. The configuration workflow supports validation, impact preflight, activation, Form/Table pre-filtering, and authoritative save enforcement. See the Reference Eligibility guide.

Hierarchy Selector configuration is available from Entity Definition Form and Table-filter authoring and from the Configuration API. It supports fixed Reference paths and recursive indexed self-References while storing only the terminal Reference value. See the Hierarchy Selector guide.

Entity UI layout remains workspace configuration rather than Net Operation Project source. Configure responsive Card item width in the Entity Definition Surface library or through the Configuration API, as described in Card item layout. Workspace C# consumes the generated entity contract but does not author or override these UI spans.

Entity Manual Order is also platform-owned configuration. Net Operation Project source does not declare the capability, choose or write its protected rank storage, or receive a direct Manual Order move service. Generated entity writes continue through the normal Moltaro save pipeline. When Manual Order is active, that pipeline automatically gives each newly inserted active record a canonical tail position in the same transaction, so ordinary generated-context creates do not need special ordering code. Positional moves remain an authenticated application/UI operation with the same authorization and concurrency boundary as other Entity actions.

For workspace-specific algorithms that need their own ordered keys, the SDK also exposes the policy-free Moltaro.Package.NET.ModuleRuntime.Ordering.LexicographicRankKeyGenerator. It returns typed generation statuses and evenly spaced sequences. Using that primitive does not grant access to Moltaro’s protected Manual Order rank, and it does not provide a Manual Order move endpoint or service.

For calendar lifecycle, effective-dated Site and Worker assignments, schedule exceptions, and effective-schedule reads, prefer IWorkScheduleAutomationCommandService and IWorkScheduleAutomationQueryService. The generated workspace contract registers both interfaces, and developer-surface marks them ApplicationAutomation and Preferred: true.

These calls run complete Work Schedule application orchestration in a separate host-owned scope. Requests cannot supply an actor or authority tier. Every command uses a stable caller-owned OperationId; calendar mutations additionally use the current RowVersion, while assignment and exception mutations use the last returned ChainVersion. Preserve an operation id only for an exact retry after an unknown outcome.

The lower-level services in Moltaro.Package.NET.WorkSchedule.Runtime remain available as advanced TrustedDirectDb, Preferred: false contracts for code that explicitly owns authorization, transaction, audit, and side effects. Read the Work Schedule C# guide before choosing that boundary.

The generated workspace contract reproduces each Entity Definition’s explicit PostgreSQL search targets through the SDK entity builder:

  • SearchField(...) declares a direct field or a path through Reference, Inverse Reference, or Table fields to a String, Text, Address, or File terminal;
  • SearchSystem(EntitySearchSystemTargetEnum, ...) declares Display name, Number, Comments, or Attachments at the root or after a relation path.

These declarations are generated build input, not user-owned Net Operation Project source. Configure search targets in the Entity Definition Search tab or through the Configuration API search contract; the next generated contract then gives Check, Build, and server-side language services the same schema metadata.

Generated String and Text properties whose Entity Definition field is labeled with the Markdown property label still expose the original Markdown source as their typed property value. Moltaro separately maintains a display-safe plain-text projection for search, presentation, projections, and audit previews. Net Operation Project code should write Markdown through the generated context and its normal SaveChanges pipeline; do not issue raw SQL against the source or its internal companion column. The save is rejected when the source exceeds the 512 KiB UTF-8 limit, and source plus projection are committed together.

A tracked direct-database update through MoltaroDbContext.SaveChanges is authoritative for every modified field. The save removes stored declarative mutation-effect ownership only from those fields and preserves ownership of unchanged fields. It does not execute configured mutation effects; those run only through the application/API Entity save pipeline.

Markdown inline images use opaque moltaro-attachment://<FileReferenceId> source tokens. Package code must not construct raw download URLs or treat a token as an access grant. The public Net Operation Project programming model does not expose draft-session adoption; use product record forms for destination-scoped image authoring.

All new attachment uploads enter PendingInspection. The descriptor exposes AvailabilityState, detected MIME, image dimensions, content generation, and the checksum/generation covered by successful inspection. Only Available content whose inspected generation and checksum still match can be downloaded through IAttachmentService. MoltaroAttachmentDownloadResult.Content is now a non-null verified Stream; dispose it after reading. Moltaro verifies the immutable body into a bounded spool before exposing any bytes, and the result does not expose BlobUri. Attachment downloads are intentionally rejected inside caller-owned database transactions so a checksum mismatch can persist its security invalidation independently. Code compiled against the previous buffered result must be updated before moving to this SDK contract.

For large Managed Cloud attachments, use UploadStreamAsync or UpsertStreamAsync with an exact declared size and a stable idempotency key. Call GetUploadCapabilitiesAsync before opening or downloading the source. It returns the effective storage type, maximum file size, multipart availability, part policy, checksum policy, actor-local active-session/reserved-byte usage and limits, any saturation retry delay, the 24-hour inactivity and 48-hour absolute session lease, and authoritative extension-aware FileTypeRules plus FileTypePolicyVersion and FileTypePolicyHash without exposing storage credentials. A rule accepts a file only when its longest matching extension and normalized declared MIME type are present together. AllowedMimeTypes is a deprecated, conservative compatibility projection and must not be used to make new upload decisions. It deliberately omits application/octet-stream; a pre-rule runtime may reject DWG/FBD after a rollback instead of allowing ambiguous bytes for every extension. A pre-session policy rejection throws MoltaroSdkException; inspect its stable FailureCode (also available as ReasonCode) instead of parsing the safe bounded message. When FailureCode is filesystem.upload.activeLimitReached, the exception has IsRetryable = true, exposes RetryAfterSeconds, and includes safe active count/limit and reserved-byte usage/limit metadata. Keep the idempotency key unchanged and retry only after the advertised delay. filesystem.file.type.notAllowed metadata includes the normalized extension, declared MIME, policy version, and policy hash so a caller can correlate the decision with the exact preflight snapshot across API and Worker processes. The rule check is not antivirus or malware inspection and cannot detect an executable file renamed to an allowed ambiguous format. Workspace administrators may deliberately configure executable extensions and own the consequences of that policy. The source may be non-seekable; Moltaro reads it in bounded parallel windows of 16 MiB parts up to the advertised MaximumParallelParts and does not expose S3 endpoints, buckets, object keys, credentials, or presigned URLs. Interactive cancellation preserves the resumable session. Host cancellation of a trusted background function aborts that physical attempt and releases capacity; an exact retry with the same logical idempotency key creates a successor. Retry the complete stream with the same key, poll GetUploadAsync, and call AbortUploadAsync only when the upload should be discarded. Uploaded means the provider accepted the body, Verifying means background verification/publication is running, and only Available returns the published reference and descriptor ids. The idempotency key and requested file-reference id identify one logical upload; UploadSessionId identifies its current physical attempt. If the attempt expired, repeat the same stream call with a fresh stream and unchanged actor, owner, metadata, key, and file-reference id. Moltaro returns one new successor session after rechecking current policy and concurrency. Querying the old session still returns terminal filesystem.upload.expired. A background attempt aborted by host cancellation may restart; explicitly aborted ordinary uploads and rejected uploads remain terminal. An already published result is replayed without uploading again. Failed with filesystem.upload.providerUnavailable is non-terminal; reopen the source and repeat the same stream call with the same idempotency key because polling does not retry a provider command. Start, status, abort, and download operations must run outside an ambient package database transaction because they can perform external storage I/O. The legacy direct-database Database/Local upload and upsert methods remain available and enforce the same workspace FileTypeRules before accepting bytes.

For a migration backlog, use UpsertStreamsAsync rather than running concurrent calls through one scoped service instance. A batch accepts at most 100 items; MaximumConcurrency is 1 through 32 and defaults to 4, while current actor upload capacity may reduce the effective concurrency. Ordinary actors remain capped at 8 active sessions; only host-established trusted system automation receives the dedicated 32-session capacity. Each item owns an independent runtime scope and transaction, results stay in input order, and one item failure does not cancel successful siblings. Each failed result carries its stable code and retry metadata. If a terminal item failure occurs after session creation, the runtime aborts the physical attempt and releases its reservation before returning the result; retryable failures retain their resumable attempt. Provide a separate caller-owned readable stream for every item and invoke the batch outside an ambient package transaction. A successful item has entered the transfer lifecycle; poll its returned upload-session id to Available before archiving the source migration task.

Trusted scheduled code may use either stream method when the historical author is unknown. UploadStreamAsync has no source-author input; for UpsertStreamAsync, leave SourceCreatedByUserId null. Moltaro preserves null on the published descriptor, file reference, and relations while keeping the runtime-owned moltaro-system-automation identity as the authorization and audit actor. A non-null source creator must resolve to an existing workspace user and never grants access. Direct-db upload/upsert follows the same unknown creator rule for the reserved system actor; ordinary direct-db calls require a real actor and attribute an omitted creator to that actor.

Use IAttachmentService.ReassignAsync to move one published attachment between EntityInstance owners without changing its FileReferenceId or copying its body. The request carries a stable OperationId, exact source and target owner identities, and the row version returned by the attachment list. A committed retry returns AlreadyApplied; the descriptor, provider object, bytes, checksum, upload attribution, and relations stay unchanged. Moltaro rejects the operation when access or Business Invariants deny detach/attach, the reference is still used by a File or nested Table<File> field, its inspected content is not currently downloadable, an upload/replacement or hold is active, or the target already contains the normalized filename. Use the stable filesystem.reassign.* failure code rather than parsing the safe message. When a configured Business Invariant provides its own stable rejection code, Moltaro preserves that code so the caller can identify the exact policy that blocked the reassignment; filesystem.reassign.invariantRejected is the fallback when the invariant provides no code. Runtime functions require the Moltaro host gateway and never call their own HTTP API or receive provider locators. A direct-db host must configure a canonical workspace name and a real Business Invariant guard, and must call reassignment outside any ambient package transaction.

This large-file contract does not change Markdown inline images. Markdown draft uploads remain image-only, at most 25 items and 100 MiB in aggregate, and use opaque moltaro-attachment:// tokens only after the existing draft workflow. Do not place a presigned URL in Markdown source.

Comment imports, reconciliation, and reassignment

Section titled “Comment imports, reconciliation, and reassignment”

Use ICommentService.UpsertAsync for an import or restore that must preserve a comment id, source creation time, source author, and relations. It cannot move an existing comment to another owner. If the source author is omitted in a host-authenticated CRON or Trigger function, Moltaro records moltaro-system-automation; an explicit source author must be an existing workspace user. The reserved actor id by itself is not trusted provenance.

When the same source comment can move between records after a previous import, use ReconcileAsync from a host-authenticated CRON or Trigger function. The operation locks the source-stable primary id and a distinct owner-scoped fallback id in one transaction. It creates, reconciles, or restores a primary that is absent or already belongs to the target; an active primary under a different owner moves in place. A deleted primary under a different owner is kept as an audit tombstone, so the operation creates, reconciles, or restores the fallback instead. Moltaro durably binds the stable primary id to the current effective comment. On a later owner change, an active bound fallback moves in place and the result is FallbackReassigned; it can therefore differ from the new owner-scoped fallback supplied by that request. If the bound effective comment was deleted, its tombstone remains and the current owner-scoped fallback is selected instead. The binding survives worker restarts and serializes competing owner changes, so one primary identity has at most one active logical comment. Missing or incompatible binding evidence fails closed. Primary-preserving branches take precedence: if their fallback is already active, the operation fails closed as an ambiguous duplicate instead of leaving two active logical comments. During an unbound first reconciliation, same-owner fallback reconciliation or restore is valid only after a foreign primary tombstone selects the fallback branch. A later deleted bound effective comment also selects the supplied fallback as described above. Always persist the typed result’s EffectiveCommentId.

The example below shows the complete identity and retry protocol. The queue stores a fresh operation id before the first call and reuses it only while the outcome is unknown. A later reconciliation cycle gets a new operation id even when source content is unchanged, so it can repair a target comment that was subsequently deleted. Primary and fallback ids remain deterministic across all cycles. Treat the returned effective id as authoritative; never assume that the fallback supplied to the current call won.

using System.Security.Cryptography;
using System.Text;
static string StableCommentId(string prefix, params string[] parts)
{
var canonical = string.Join(
"\n",
parts.Select(part => $"{Encoding.UTF8.GetByteCount(part)}:{part}"));
var digest = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(canonical)))
.ToLowerInvariant();
return $"{prefix}-{digest[..40]}";
}
var targetOwner = MoltaroResourceIdentity.BoardItem(boardId, boardItemId);
var primaryCommentId = StableCommentId(
"legacy-comment",
legacySystemKey,
legacyComment.Id);
var fallbackCommentId = StableCommentId(
"legacy-comment-owner",
legacySystemKey,
legacyComment.Id,
targetOwner.ModuleKey,
targetOwner.ResourceKind,
targetOwner.ContextResourceId,
targetOwner.ResourceObjectId);
// Persist before calling. Reuse only after a timeout or another unknown outcome.
var operationId = queueItem.PendingOperationId
?? Guid.NewGuid().ToString("N");
await queue.SavePendingOperationAsync(queueItem.Id, operationId, cancellationToken);
var reconciled = await comments.ReconcileAsync(
new MoltaroCommentReconcileRequest
{
OperationId = operationId,
PrimaryCommentId = primaryCommentId,
FallbackCommentId = fallbackCommentId,
TargetOwner = targetOwner,
CommentText = legacyComment.Text,
SourceCreatedByUserId = mappedWorkspaceUserId, // Omit for trusted system-automation attribution.
SourceCreatedAt = legacyComment.CreatedAt,
QuotedCommentId = mappedQuotedCommentId,
Relations = relations
},
cancellationToken);
await queue.MarkCommentReconciledAsync(
queueItem.Id,
reconciled.EffectiveCommentId,
reconciled.RowVersion,
cancellationToken);

Exact replay returns the original outcome, effective id, and row version with IsIdempotentReplay = true and performs no comment, relation, receipt, or event write. Reusing one operation id with changed content, owner, author, timestamp, quote, or relation set fails with ReconcileOperationConflict. An incompatible fallback binding fails with ReconcileCandidateConflict instead of adopting or disclosing the other comment. User-action functions and external direct-db hosts cannot invoke reconciliation.

Once an id appears as a primary, fallback, or effective identity in durable reconciliation evidence, general UpsertAsync and independent ReassignAsync calls reject it with ReconciliationIdentityReserved. Keep that logical import on ReconcileAsync. Ordinary update/delete calls share its identity lock and authorize the current owner inside their transaction, so a concurrent owner move cannot apply a mutation authorized against the previous owner.

For a deletion decision based on a read, inspect current.ActiveRelations as well as content and attribution, then use the conditional overload:

var current = await comments.GetAsync(commentId, cancellationToken)
?? throw new InvalidOperationException("The comment no longer exists.");
// Apply your application's preservation checks to this exact snapshot first.
await comments.DeleteAsync(
new MoltaroCommentDeleteRequest(current.Id, current.Owner, current.RowVersion),
cancellationToken);

The owner and non-empty version are checked inside the normal deletion transaction. A conflict leaves the comment and its relations intact; re-read and reconsider the decision. The string overload deletes current state and does not bind deletion to an earlier inspection. These overloads keep the same authorization rules and are available through runtime and trusted direct-db services. ActiveRelations contains all active relation snapshots in service reads; custom EF queries do not load relations automatically. The token covers the comment, not another canonical comment, a board target, or file lifecycle. For cleanup that requires no file links, inspect the read-only comment/file link projection too: the service’s Files hides deleted, potentially restorable placements even when an active link remains.

CRON and Trigger functions can use ReassignAsync to atomically move one active comment between existing EntityInstance or BoardItem owners. The comment id, text, quote, source attribution/time, and relations remain unchanged. Supply a stable operation id and the current comment row version. Replaying that exact request returns AlreadyApplied without duplicate writes or events. User-action functions and external direct-db hosts cannot use this operation, and a reconciliation-reserved comment must remain on the reconciliation path.

var current = await comments.GetAsync(commentId, cancellationToken)
?? throw new InvalidOperationException("The source comment no longer exists.");
var moved = await comments.ReassignAsync(
new MoltaroCommentReassignRequest
{
OperationId = operationId,
CommentId = current.Id,
SourceOwner = MoltaroResourceIdentity.BoardItem(sourceBoardId, sourceItemId),
TargetOwner = MoltaroResourceIdentity.BoardItem(targetBoardId, targetItemId),
ExpectedRowVersion = current.RowVersion
},
cancellationToken);

Keep the operation id only for the exact retry. On stale input, re-read and start a new operation. Handle MoltaroSdkException.ReasonCode using MoltaroCommentErrorCodes; do not parse error messages.

Principal CRUD and application automation services

Section titled “Principal CRUD and application automation services”

For workspace C# that changes principals, use the existing principal CRUD services. Boards and Entitlement Operations retain their curated application-level services:

  • IUserService, IRoleService, and IGroupService;
  • IBoardAutomationCommandService and IBoardAutomationQueryService;
  • IEntitlementAutomationCommandService and IEntitlementAutomationQueryService.

Principal mutations create their own transaction, serialize with authoritative Entity authorization through the workspace access-state lock, and atomically commit the principal rows, cache invalidation version, and applicable user-index outbox row. They are not supported inside an already active package transaction.

MoltaroRole.RowVersion versions the complete permission set, including an empty set. IRoleService.ReplacePermissionsAsync requires that value in MoltaroReplaceRolePermissionsRequest; stale input fails with roles.permissions.staleRowVersion without changing state, while exact replay is write-free. Scheduled jobs and other hosted Package or Net Operation Project functions can inject IRoleService directly. Permission changes validate against the installation’s current permission catalog, including dynamic permissions; your startup does not need to register a permission directory. Prefer atomic GrantPermissionAsync and RevokePermissionAsync for additive integrations. External direct-db hosts must populate MoltaroCoreServicesOptions.KnownPermissionKeys; the default empty catalog and unknown keys fail closed.

Entity application automation uses IEntityAutomationCommandService. Its exact 15 commands are CreateAsync, UpdateAsync, ArchiveAsync, RestoreAsync, DeleteAsync, MergeAsync, ImportAsync, BulkUpdateAsync, ReplaceResponsibilitiesAsync, ReplaceTagsAsync, CreateCommentAsync, UpdateCommentAsync, DeleteCommentAsync, UploadAttachmentAsync, and DeleteAttachmentAsync. Every request carries an OperationKey; exact replay returns the stored authorized result, while reusing the key for different input returns an idempotency conflict. Import and bulk update accept at most 100 items per captured transaction. UpdateAsync, bulk-update items and import update items follow the HTTP PATCH rule: an item that writes a field creates a new record version even when its values equal the stored ones, so send only changed fields (see Update with optimistic concurrency).

For record import and field history, start with Import records and retain history. It compares application commands with direct EF, explicit business events and historical audit import. The command operates on one definition; outer import success does not mean every item succeeded. The guide includes a compile-checked example and transaction/replay limitations.

Create, update, import and bulk-update items record the function run that issued the command as the change source of each history entry: Function, Trigger for a trigger function or Schedule for a scheduled run, with the executed version and the user who started the run when there is one. These writes do not start Entity trigger functions of the records they change, so call any follow-up logic explicitly.

CreateAsync takes the five-member EntityCreateCommandRequest: a non-empty UUID OperationKey, the matching EntityDefinitionKey, a Values field map, the latest ExpectedDefinitionRowVersion, and a non-null ResponsibilityCandidates list. Use Array.Empty<ResponsibilityCandidatePayload>() when the caller proposes no StageOnCreate assignments. Preserve the operation key only for an exact retry; re-read the Entity Definition before rebuilding a request after a schema concurrency conflict.

Code written for the earlier create shape must replace Fields with Values and replace string MutationIdempotencyKey values with caller-generated UUID OperationKey values. Rebuild against the current SDK rather than maintaining an adapter for the removed request shape.

Each method returns SecurityMutationCommandResult<TResult>. Success values are the documented authorized-detail, safe identity/receipt, responsibility, tag, comment, attachment, import, or bulk result unions. A committed Entity record mutation whose result cannot be shown answers EntityRecordMutationReceipt, and a responsibility replacement EntityResponsibilityMutationReceipt; both derive from MutationReceipt, carry a typed CallerKnownIdentity, and keep the JSON discriminator "Receipt". Tag, comment, and attachment mutations still answer ResourceMutationReceipt, so match each union on its own receipt type. Current Entity View and field visibility are rechecked before replay output, so a retry cannot reveal a record or field that is no longer readable; such a replay answers a receipt with IsIdempotentReplay = true, and a Create replay fails with moltaro.instances.replay.resultNotViewable because its record identity was never known to the caller.

The typed receipts came with Package gateway contract 12 and Net Operation Project gateway contract 9; the current SDK uses 16 and 13. Rebuild and republish every package and project against the current SDK: the host refuses an artifact built for another contract instead of running code whose receipt checks would silently stop matching (see Rebuild after an SDK contract change). Search your code for ResourceMutationReceipt rather than relying on the compiler alone: a plain is ResourceMutationReceipt test on an Entity result compiles with only a warning and is always false. This facade is host-enforced and always requires the current Entity Security configuration and dependency guards. Its host-authenticated system actor bypasses end-user permission statements but not configuration existence, lifecycle invariants, concurrency, or idempotency; the reserved actor id without trusted provenance grants no access. It is distinct from trusted direct-database services, which must never be substituted behind an application-facade call.

For record lifecycle automation, inject IEntityAutomationCommandService and pass EntityIdentityCommandRequest to ArchiveAsync or RestoreAsync:

var request = new EntityIdentityCommandRequest(
operationKey,
"service_ticket",
ticketId,
currentRowVersion,
"Closed by retention policy");
var result = await entities.ArchiveAsync(request, cancellationToken);
if (!result.IsSuccess)
{
var codes = result.Errors.Select(error => error.Code).ToArray();
return FunctionResult.Failed(string.Join(", ", codes));
}
var receipt = (EntityLifecycleMutationReceipt)result.Result!;
var nextRowVersion = receipt.Data.RowVersion;
var replayed = receipt.IsIdempotentReplay;

Preserve operationKey only for an exact retry and use the record’s current RowVersion. moltaro.instances.notFound is the safe missing-or-not-visible failure, moltaro.security.concurrency.staleRowVersion (Field RowVersion) means the row version is stale, moltaro.instances.conflict means that a concurrent write holds the record, and moltaro.instances.archive.alreadyArchived / moltaro.instances.restore.notArchived report lifecycle-state conflicts. moltaro.instances.reason.required and moltaro.instances.reason.tooLong (Field Reason) reject a missing, blank or longer than 1000-character reason before any write; the trimmed reason is stored with the operation and shown in record history. The installation’s GET /api/workspace/error-catalog describes the REST answers of these operations; C# command results are not covered by it and can report other codes for the same condition, for example moltaro.security.request.invalid for a malformed command instead of the REST request codes. Success always returns an EntityLifecycleMutationReceipt; its identity-only Data contains the new committed RowVersion without disclosing Entity field values. The receipt exposes IsIdempotentReplay, and a replay does not emit another audit row or resource event. Permanent DeleteAsync applies the same reason rules. MergeAsync requires EntityMergeCommandRequest.Reason with the same bounds and stores it with the archive of every merged source record. The effective actor of this facade is always moltaro-system-automation. Scheduled Jobs may run without an original user; when an interactive user exists, that identity and the function, run, correlation, and Schedule/Trigger/Function origin remain audit attribution.

ReplaceResponsibilitiesAsync replaces the complete assignment timeline of one responsibility on one record. Current subjects that are left out of Subjects are closed; keep the EffectiveFrom of subjects that stay, because a new instant closes and reopens their assignment. EntityReplaceResponsibilitiesCommandRequest.RowVersion is the record’s responsibility state version, ResourceRowVersion. All responsibilities of the record share it, and it is not the record’s row version. A record without any responsibility state (none of its responsibilities was ever assigned, including at creation) reports the empty UUID, so its first assignment passes Guid.Empty:

var request = new EntityReplaceResponsibilitiesCommandRequest(
operationKey,
new ResourceIdentity(
MoltaroResourceModuleKeys.CoreEntity,
MoltaroResourceKinds.EntityInstance,
definitionId,
ticketId),
"Owner",
[new ResponsibilitySubjectPayload("User", ownerUserId, DateTimeOffset.UtcNow, null)],
Guid.Empty,
"Assigned by intake automation");
var result = await entities.ReplaceResponsibilitiesAsync(request, cancellationToken);

Afterwards pass the ResourceRowVersion of the latest replace result on that record, for any of its responsibilities. The C# facade has no read for this version; when the record already has state that your code did not write, such as assignments made at creation, read it from the current responsibility state over HTTP. A value other than the current one, including Guid.Empty once the record has responsibility state, fails without changing the assignments; re-read the state and start a new operation. The reason must contain 1–1000 characters after trimming. An exact retry repeats the same request, including its EffectiveFrom values, and returns the stored state without another write. Reusing the key for a different request on the same record and responsibility fails with moltaro.security.idempotency.keyReuse.

Board Statements are workspace configuration authored through the Boards Configuration API, not a new C# evaluator or Net Operation Project service. Their public Boards package schema exists for trusted direct-database module integration, while supported workspace logic should use the documented configuration endpoints and server-owned DSL authoring profile.

Board automation facade mutations enforce enabled transition, entry, exit, and destination status-invariant Constraints through the same application transaction as the Runtime API. BoardAutomationMoveValidationResult exposes ordered Errors, CapturedAtUtc, and ConstraintDecisions in addition to compatibility ErrorCodes; callers must handle every returned error and must not parse the workspace-authored message. The advanced direct-database Boards package also exposes IBoardConstraintRuntimeEvaluator and AddMoltaroBoardsConstraintRuntimeEvaluator<TEvaluator>() for trusted hosts. This public custom-evaluator contract handles Board events only. It cannot opt a standalone host into proposed-state StatusInvariant evaluation, which remains fail-closed outside Moltaro’s host-provided integration. Its default event evaluator fails closed if an enabled Binding applies without a registered evaluator. Destination pickers should call IBoardRuntimeCommandService.ValidateMoveOptionsAsync; it evaluates candidates for one item and actor as one request-local batch. Custom high-volume evaluators should override EvaluateBatchAsync. Authoritative mutations call EvaluateAuthoritativeBatchAsync only after acquiring governance locks. An evaluator backed by a configuration or compiled-plan cache must override that method and reload the applicable configuration inside that call; it may delegate to EvaluateBatchAsync only when its evaluation source is uncached and already authoritative. Custom definition providers must implement the remaining set-based Board definition lookups. BoardRuntimeActor carries only the user id; roles, groups, responsibilities, Statements, and exact transition permissions come from the host’s captured SQL authorization scope.

Direct package move validation also returns ordered transition LinkPrerequisites with current and missing counts. A trusted host can set MoveBoardRuntimeItemRequest.PrerequisiteLinks to existing Board item ids; the package derives link kind/direction from the transition, applies ordinary link permissions and invariants, and commits any new link with the move. Inline target creation remains an application/Runtime API concern rather than a direct-package contract.

Composite Entity mutations as the current user

Section titled “Composite Entity mutations as the current user”

IEntityCompositeMutationService creates and updates standalone Entity records, up to the host’s step limit (100 by default), in one transaction: every step commits or none does. Unlike IEntityAutomationCommandService, it runs as the user the host admitted for the current run, with that user’s Entity Security, and never as moltaro-system-automation:

  • a Command runs as the user who invoked it;
  • an HTTP endpoint runs as the bearer user (AuthenticatedUser) or the configured Service user (ConfiguredServiceUser);
  • Jobs, schedules, triggers, Actions and other runs without an admitted current user are refused with moltaro.automation.entity.currentUserInvocationRequired, and a call while the function’s database context has an open transaction with moltaro.automation.entity.ambientTransactionUnsupported.

ExecuteAsync takes an EntityCompositeMutation with a caller-owned OperationKey and ordered EntityCompositeCreateStep and EntityCompositeUpdateStep items, and returns SecurityMutationCommandResult<EntityCompositeMutationResult> with one EntityCompositeRecordIdentity or EntityCompositeRecordReceipt per step. Errors name the step in Field (such as Steps[1].RowVersion) and in Metadata.StepId. The steps, the host’s limits, replay and history follow the REST operation; EntityCompositeMutationLimits holds the limits every host accepts (16 steps, 1,048,576 bytes), not this host’s own; the same user’s REST request and C# call share the OperationKey journal. Writes of this service do not start Entity trigger functions. The Net Operation Project registers the service automatically. The rules, a compile-checked Command and the error handling are in Composite Entity mutations.

When code opens a generated-context transaction directly, call AcquireMoltaroDynamicSchemaReadLockAsync immediately after BeginTransactionAsync and before any query, row lock, or mutation. Use the synchronous pair for synchronous transactions. The lock prevents a NetPackage schema update from changing the meaning of the generated EF model during the unit of work. ExecuteInTransactionIfNotExistsAsync takes this lock automatically before it invokes package code.

Normal generated-context SaveChanges and SaveChangesAsync also enforce status invariants when an existing Entity is the Target or Board Data of a governing Board Item. Moltaro evaluates the complete tracked proposed root and owned-table state, including owned Address and Money changes, before persistence. Single Select values use one option key and Multi-Select values use an array of option keys. The generated workspace contract maps a sanitized C# enum member back to the actual workspace option key through a platform-owned EF converter; for example, member InProgress can represent key in_progress. Board DSL and Runtime API values use the workspace option key, not the sanitized member name. Generated String fields carrying the TimeZone semantic role retain that role in direct-database audit evidence. Normal generated-context SaveChanges and SaveChangesAsync canonicalize IANA aliases and reject values outside the platform catalog before persistence, including owned table rows. Raw SQL bypasses this protection and is not a supported mutation path. File values use stable string ids. Upload directly through IFileFieldService, wait for Available, and then save and read back the field reference. IAttachmentService owns the separate resource attachments path. See File fields and attachments for scheduled imports, destination permissions and replay.

Money uses an Amount/CurrencyCode object: a missing Amount makes the whole value canonical null, while an omitted, null, empty, or whitespace CurrencyCode uses the field’s normalized uppercase BaseCurrencyCode. An Address with no meaningful text or coordinate component is canonical null. Persisted and proposed values use the same carriers as the Runtime API. A rejection rolls back the save and throws MoltaroPreCommitSaveRejectedException; its Errors collection contains the safe, ordered failures from every registered provider, including all governing active Boards. Catch that typed exception when function code needs to translate a rejected save into its own result. Deleting a record that another module still depends on, for example the Context of an unfinished task, is rejected the same way: the dependent module’s errors are then followed by moltaro.instances.delete.referenced.

The exception is public from Moltaro.Package.NET.ModuleRuntime.Runtime. The following fragment is intended for an OnRunAsync method that returns FunctionResult:

using System.Linq;
using Moltaro.Package.NET.Functions;
using Moltaro.Package.NET.ModuleRuntime.Runtime;
try
{
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (MoltaroPreCommitSaveRejectedException exception)
{
return FunctionResult.OkData(new
{
Accepted = false,
Errors = exception.Errors.Select(error => new
{
error.Code,
error.Target,
error.Metadata,
}),
});
}

Branch on Errors[].Code, not Message. A run that does not catch the exception receives the general run-level failure reason moltaro.preCommit.rejected; the provider-specific reasons remain the ordered codes in Errors because one save may be rejected for more than one reason. Moltaro persists up to 50 ordered entries in the function Run detail, caps FieldKey/Code at 128 UTF-16 code units and Message at 1024, caps the JSONB envelope at 131,072 UTF-16 code units, and reports any clipping with ValidationErrorsTruncated. Queued and scheduled rejections are terminal Failed outcomes, so do not enqueue a replacement as a transport retry. The Function operations Run drawer shows the retained entries and links to the owning queued job. Entity-context diagnostics are projected through current Entity row and field-read authorization, so restricted or unknown field entries are omitted for callers who cannot read those fields. Fieldless entries require visibility of every active field because no narrower field boundary is available. The protected correlated DevLog event contains the same retained ordered FieldKey/Code pairs plus counts and truncation, but never validation messages, business values, SQL, secrets, stacks, or the raw exception. BoardConstraintSaveRejectedException is a separate exception type used only by direct Boards runtime commands and protected direct Board mutations; code that handles both boundaries must catch both types explicitly. Multiple tracked saves inside one explicit transaction retain the latest proposed Entity state from earlier successful saves. The first Entity mutation lock batch fixes the transaction’s complete Entity identity set, including a Board-command batch or a save later rejected by validation. Every later batch must be a subset. A new identity fails before lock acquisition with moltaro.preCommit.newEntityIdentityAfterLockBatchUnsupported; stage all records in a cross-Entity unit of work and save them in one call. This lock-batch rule applies to every generated-context Entity transaction, even when none of the records is used by a Board. The complete sorted identity set is fixed before locks are acquired so concurrent transactions cannot each hold one Entity lock while waiting for the other.

Manual Order adds the same fail-closed transaction rule for its separate lock. If an Entity mutation lock has already been acquired, a later save cannot first introduce a create, archive, restore, or delete that needs the Entity’s Manual Order lock. The SDK throws MoltaroPreCommitSaveRejectedException before lock acquisition with code moltaro.preCommit.manualOrderAfterEntityLockUnsupported. Stage that lifecycle operation in the first save, or roll back/complete the current work and retry it in a new transaction. Catch the typed exception and inspect Errors; do not parse its message.

New generated Entity ids are application-assigned strings. For an atomic multi-record import, assign every id first, connect References/foreign keys in memory, add the complete graph to the context, and call SaveChangesAsync once. A dependent record does not need an intermediate save merely to discover its parent’s id. Prefer source-stable ids or a durable source-id mapping when the import must be safely repeatable. See Create a related Entity graph in one atomic save. A direct generated-context save is trusted code without Entity Security; to save several standalone records atomically with the caller’s own access, history and replay, a Command or HTTP endpoint uses IEntityCompositeMutationService instead.

An ordinary single-savepoint rollback restores that snapshot: Entity state saved before the savepoint remains visible, while state saved after it is discarded. If nested savepoint history is ambiguous to the runtime, every later Entity save fails closed with moltaro.preCommit.transactionOverlayInvalidated; roll back the transaction and retry the complete unit of work in a new transaction. MoltaroDbContext.SaveChanges[Async] creates that savepoint before the OnBeforeSaveChanges[Async] hook. Database work performed by the hook, runtime preflight, the Entity write, mutation-effect ownership release, audit, and runtime outbox changes therefore share the same rollback boundary. An owned table row’s OwnerEntityInstanceId is immutable after insertion. Do not parse exception text or expose evaluated field values.

The same save boundary validates newly assigned File fields on Entity roots and table rows. The id must resolve to a non-deleted file reference and descriptor whose content is Available or LegacyUninspected. Pending, rejected, soft-deleted, and nonexistent references are rejected through MoltaroPreCommitSaveRejectedException with the safe code filesystem.file.content.unavailable. An unchanged existing value does not block an unrelated edit.

ExecuteUpdate[Async] and ExecuteDelete[Async] over a MoltaroEntity or one of its owned table-row types produce analyzer error MOLTARO039: EF bulk operations bypass interceptors and therefore cannot participate in pre-commit validation, audit, presentation, or runtime side effects. Direct SQL that bypasses the generated MoltaroDbContext is unsupported.

Do not add, edit, or delete BoardItem rows through a generated context. Tracked direct writes are rejected at save time; ExecuteUpdate[Async] and ExecuteDelete[Async] over BoardItem produce analyzer error MOLTAROBOARD001. Use IBoardRuntimeCommandService so locking, Constraints, history, audit, and outbox behavior stay atomic. Raw SQL against Board runtime tables is outside the supported Package SDK contract.

Inside one explicit package transaction, call a Board runtime command before staging or saving any Entity or owned-table mutation. A direct Boards runtime service call throws BoardConstraintSaveRejectedException, and a Net Operation Project Board automation facade returns an error, with moltaroBoards.runtime.entityMutationBeforeBoardCommandUnsupported when this ordering is violated. The facade performs this check before opening its separate application scope, so it cannot wait on locks held by the calling transaction. Internal Moltaro orchestration may use configuration-prelocked paths that are not part of the Package SDK contract.

Board configuration is admin-owned. Tracked direct writes to Board, Statement, Constraint, Binding, Status, Transition, Target Definition, and related configuration rows are rejected at save time. Bulk writes over those types produce MOLTAROBOARD002. Use the Board Configuration API so advisory locking, DSL validation, active-board health checks, and cache invalidation are part of the same transaction.

Historical Board import and reconciliation do not replay transition, entry, or exit Constraints against the current actor or clock. Only the resulting status invariant governs that path, and it is enforced before commit.

Principal CRUD accepts caller-supplied stable string ids. Use an external source id as MoltaroCreateUserRequest.Id, MoltaroCreateRoleRequest.Id, or MoltaroCreateGroupRequest.Id, call the matching Get*Async, then choose Create*Async or Update*Async. These ids are the persisted Moltaro ids, not separate external keys. There is no separate synchronization facade, upsert result family, or preflight service.

The exact Unicode UserName is stored without trimming or rewriting; lookup uniqueness uses trim + Unicode NFC + invariant uppercase. User create/update can apply profile, type, email confirmation, blocked state, complete role membership, and metadata in one operation. MoltaroUpdateUserRequest.RoleIds is applied literally — an explicit list revokes every role missing from it, including Admin and Configurator, while null preserves the current memberships and Everyone is always retained. UserType is nullable and null keeps the stored type. Credential activation remains explicit. Role and group create/update use the same stable-id CRUD pattern, and built-in system role ids cannot be created, renamed, or deleted.

These services signal failure with ordinary .NET exceptions — ArgumentException for malformed input and InvalidOperationException for a rejected operation such as a duplicate id, a protected role, or an Owner change. They carry English messages and no stable code, so check state with the matching Get*Async before writing instead of parsing exception text. The stable ApiError codes documented for users and roles belong to the workspace HTTP API.

ITagService.ReplaceAsync uses the current execution actor for newly added tags. Scheduled automation is therefore attributed to moltaro-system-automation without creating a synthetic workspace user. Set SourceCreatedByUserId only when an import or restore preserves an existing workspace user as the historical author. An unknown source user throws MoltaroSdkException with the stable MoltaroTagErrorCodes.SourceCreatorNotFound reason code; function runs persist that value as FailureReasonCode and do not expose database constraint details. Any other effective actor must identify a real workspace user or the operation fails with MoltaroTagErrorCodes.ActorNotFound. Concurrent replacements for the same resource are serialized, retries are idempotent, and tag changes emit TagAdded, TagUpdated, or TagDeleted resource events in the same transaction. Moltaro uses those events to retain function origin metadata, update entity activity, and produce Board item audit/outbox side effects. Scheduled automation is authorized through the platform’s owner/admin system scope only while the runtime-owned tag mutation guard is active and the function invocation carries platform-issued trusted-system provenance. Actor strings from HTTP calls or persisted resource events cannot create that provenance. The actor id is reserved from workspace-user creation and by a database constraint, so supplying the string alone does not grant access. Upgrades reject a legacy user row with that id until the conflicting user is renamed or removed. OriginalUserId is attribution and does not grant access. Direct-db tag events retain original-user and correlation metadata as a non-function Moltaro.Tags source. MoltaroTag.CreatedByUserId can be null when reading legacy rows created before author attribution was required.

Entity and Board tag flags are deliberately separate. An EntityInstance owner uses EntityDefinition.TagsEnabled; a BoardItem owner uses Board.TagsEnabled. Ordinary access failures expose stable MoltaroTagErrorCodes for disabled Entity tags, disabled Board tags, terminal Board items that require historical reconciliation, access denial, unknown owners, and unsupported owner kinds. The workspace Tags HTTP API returns the same safe code and owner metadata in correlated Problem Details.

While a user has a current external-identity link, its identity-profile state remains provider-owned; blocked state, role membership, and ExtendedData remain mutable. DeleteUserAsync archives the current external-identity link before soft-deleting the user so that the external subject can be linked again.

User, role, and responsibility-group DTOs expose ExtendedData for small non-secret integration metadata. Keys should be source-namespaced, for example ventcontrol.source-id. A null request value preserves existing metadata; an empty dictionary clears it. The supported limit is 32 string entries, key length 128, and value length 2048. moltaro.* and moltaro:* keys are reserved. Extended data is read back by list/get calls, is not a permission grant, and must not contain credentials, tokens, secrets, or full source payloads.

The Board and Entitlement application services execute complete Moltaro application orchestration in a separate scope as moltaro-system-automation, while preserving original-user, function-run, and correlation metadata. Command DTOs do not accept ActorUserId. Entitlement command DTOs require a stable IdempotencyKey. AddBoardAutomationItemRequest requires a caller-owned UUID OperationKey: preserve it for an exact retry, which returns the original item without another write; changed-payload reuse conflicts. Other ordinary Boards commands mirror Runtime API concurrency and repeat semantics without a separate receipt. Rebuild existing consumers and pass the UUID as the first AddBoardAutomationItemRequest constructor argument; the older constructor without an operation key is no longer available. Historical Board migration is the exception: ImportHistoricalItemsAsync uses durable, case-sensitive per-Board source keys and GetHistoricalImportAsync resolves unknown outcomes. Batches contain 1–500 items, commit per item, return ordered partial results, and require the latest ExpectedRowVersion only when reconciling changed state. Exact replay returns Unchanged without additional writes. Reconciliation updates the existing Board Data record; its creation-time DataRecordId cannot be replaced. The separate ReconcileHistoricalItemDataAsync operation repairs only Subject/Description/Part values on terminal, non-removed, source-identified historical items. It requires exactly one BoardItemId or case-sensitive SourceKey locator plus ExpectedRowVersion; changed stale patches roll back, while an exact replay is write-free and returns the current token. Omitting a BoardHistoricalTextPatch preserves that text value and wrapping null clears it. The operation preserves Board lifecycle, target, history, and timestamps; ordinary closed-item UpdateDataAsync remains forbidden. A pre-existing Entity Definition Board item uses the explicit GetHistoricalRecoveryStateAsync then RecoverHistoricalItemAsync flow with the returned exact item id, row version, and recommended adoption or post-removal mode. Self-contained, Entitlement, and ambiguous multi-pass targets are rejected by recovery. These services are supported only in Action, TriggerHandler, Command, Job, and HttpEndpoint functions. Validation and BeforeSaveMutation receive the stable moltaro.automation.executionPhase.unsupported error.

ReconcileHistoricalItemTagsAsync is the terminal-only tag counterpart. Its 1-500 item batch commits per owner and returns ordered partial results with structured MoltaroRuntimeError code, field, target, and metadata. Every item uses exactly one Board item id or historical source key and a row-version guard. Add creates only missing names. ReplaceManagedSet changes only the explicit ManagedTagNames set, preserving all manual/unrelated tags; an empty desired tag list clears that managed set. Exact replay is write-free; lifecycle, Board Data, completion/history, and item row version are unchanged. Normal tag resource events carry the system actor and function/run/correlation origin into Board audit and outbox processing.

Ordinary SDK tag mutation rejection throws MoltaroSdkException with the same stable owner-specific ReasonCode/FailureCode, optional Field, and safe Metadata used by the HTTP and automation contracts. Callers should branch on the code and metadata, never on the diagnostic message.

The *.Runtime module services remain advanced trusted direct-DB APIs. They are useful when package-style code deliberately owns all application side effects, but they are not the default for triggers or jobs. See the C# runtime recipes and read ServiceKind/Preferred in the installation’s developer-surface response.

Advanced direct-DB code that uses ExecuteInTransactionIfNotExistsAsync receives one atomic boundary in both hosting modes. The helper opens and owns a transaction when none exists and acquires the shared dynamic-schema lock before invoking the action. When the caller already owns a transaction, it acquires the same lock before creating a nested savepoint; an exception or a result rejected by shouldCommit restores both persisted changes and the EF tracked state to that savepoint. The caller may then decide whether to commit or roll back its outer transaction.

Moltaro.Package.NET.Functions.ISecretService is available through constructor injection in Job, Command, Action, TriggerHandler, Validation, BeforeSaveMutation, and HttpEndpoint functions:

public sealed class SendToPartner(ISecretService secrets) : GlobalFunction
{
public override Task<FunctionResult> OnRunAsync(CancellationToken cancellationToken)
{
var token = secrets.GetRequired("integrations.partner.api-token");
// Use token only for the outbound call. Do not log or return it.
return Task.FromResult(FunctionResult.Ok("Partner call completed."));
}
}

GetRequired throws SecretUnavailableException; inspect Code against SecretUnavailableCodes.Missing, Disabled, and Retired. TryGet returns false with a null value for those states. Keys are case-insensitive, but null, empty, whitespace-padded, or null-character-containing keys throw ArgumentException.

The runtime captures one immutable snapshot before it creates the function. Rotation affects the next invocation, not repeated lookups in the current one. The service is a trusted-code boundary: any published trusted C# that knows a key can resolve it. Never place a value in arguments, results, DevLog, diagnostics, audit data, exports, or exception messages.

The canonical Managed secrets guide covers provisioning, lifecycle, audit, stable codes, safe token handling, agent rules, and a compile-checked Job example.

  • ValidationFunction<TEntity> — rejects an entity save with validation issues. Bound to save operations (create, update, archive, restore, delete) with [EntitySaveBinding(...)]. See entity-scoped logic.
  • MutationFunction<TEntity> — changes a record before create/update save, or runs after an operation when bound as a trigger with [EntityTrigger(...)]; covered on the same page.
  • GlobalFunction / GlobalFunction<TArgs> — reusable logic without an entity binding. See global logic.
  • CommandFunction<TResult> / CommandFunction<TArgs, TResult> — synchronous calls through the authenticated commands API. See commands and API functions.
  • GlobalActionFunction<TInput>, EntityInstanceActionFunction<TEntity, TInput>, EntitySelectionActionFunction<TEntity, TInput>, and EntityCollectionActionFunction<TEntity, TInput> — user-triggered UI actions with typed input. See actions.
  • HttpEndpointFunction / HttpEndpointFunction<TBody> — inbound HTTP endpoints served under /api/workspace/inbound/{endpointKey}, declared with [MoltaroHttpEndpoint]. See HTTP endpoints.

TEntity is the workspace entity class the function works with; argument, input, and result types are JSON-serializable records you define.

For generated workspace entities, a Date and time field is a nullable System.DateTimeOffset property backed by one PostgreSQL timestamp(6) with time zone column. SDK SaveChanges normalizes values to UTC and truncates precision beyond microseconds. The original numeric offset is not a persisted attribute of the field.

The SDK exposes the supported object-level Business Invariant contract under Moltaro.Package.NET.BusinessInvariants. It defines the generic BusinessInvariant<TEvaluationData> authoring base, the no-data BusinessInvariant base, canonical resource and actor inputs, operation-key constants, denial results, and the explicit IBusinessInvariantValidator contract for function code.

Build analysis validates stable keys, typed targets, exact/category operation scopes and exceptions, declared denial messages, parameter schema, supported dependencies, and pure Grant code, including side effects reached through source-defined helper methods. Package and Net Operation Project publication produce equivalent deterministic descriptors and resolve embedded default text without creating configuration.

Discovery alone does not enable an invariant; administrators own target configuration. Enabled compatible targets shape current object-operation decisions and automatically guard admitted Entity mutation owners. Function execution is not intercepted automatically. Inject IBusinessInvariantValidator, validate the exact existing resource and function.{BusinessFunction.Key} with empty parameters, and stop before side effects when the ordinary ValidationResult is invalid.

When an owning runtime adapter returns an invariant denial, its public API error keeps the exact operation key in Target and the stable invariant source key in Metadata.SourceKey.

Use the published constants instead of private operation strings. Board cycle assignment and rollover both use BoardItemObjectOperationKeys.AssignCycle; rollover validates every affected open item before moving any of them.

Every function class carries [MoltaroFunction(id)]. The id is the durable function id; an optional Key overrides the callable key (by default the id is the key). Name and Description are shown to administrators and configurators — set Description on any published function so its purpose is clear in the Function Catalog. EnabledByDefault controls whether the function starts enabled when it is first applied.

Publishing commands and enqueueable functions

Section titled “Publishing commands and enqueueable functions”

[MoltaroCommand] publishes a command function at POST /api/workspace/commands/{functionKey}. [MoltaroApiEnqueue] publishes a Job-contract global function for asynchronous enqueue at POST /api/workspace/functions/{functionKey}/enqueue; callers with access can also observe and cancel the resulting queued job.

Both attributes are fail-closed: exactly one of PermissionKey or AllowAnyAuthenticatedUser = true must be configured; a publication without an access declaration is rejected at build and apply time, and anonymous invocation is never supported. A permission key is a lowercase dotted identifier such as acme.commands.recalculate — the segment after the last dot becomes the catalog code, the leading segments the catalog group, so administrators can assign it to roles.

MoltaroCommand.TimeoutSeconds optionally overrides the synchronous timeout, from 1 through 86400 seconds. When unset, the runtime action timeout setting applies — and the effective timeout never exceeds that installation setting.

MoltaroCommand.LicenseUsageMutation declares how the command can affect licensed workspace usage. It defaults to MoltaroLicenseUsageMutationKindEnum.Growth, which cannot run when the latest background usage count already reaches a license limit. Use Neutral only when the complete command cannot change licensed usage. Use ReducesUsage only when it can exclusively release usage; this kind remains available at a limit. Usage is counted in the background, by default every five minutes; the check before a command only reads the latest count, takes no lock and never waits, so growth committed after that count can exceed a limit until the next count.

Existing command publications are migrated as Growth. Adding a Neutral or ReducesUsage declaration takes effect only after the Package or Net Operation Project is rebuilt and applied, which synchronizes the new descriptor into the active publication. Persisted publication rows are not an authoring surface. The descriptor came with Package gateway contract 11 and Net Operation Project gateway contract 8; the current SDK uses 16 and 13, and the host runs only artifacts of its own contract. Source artifacts must be rebuilt against this SDK; see Rebuild after an SDK contract change.

MoltaroApiEnqueue.TimeoutSeconds optionally declares the worker timeout for the queued job, also from 1 through 86400 seconds. Moltaro captures the value when the API request creates the job, so every retry uses the same timeout even if the publication or runtime defaults change. When unset, the runtime Job timeout setting applies. API enqueue is always treated as licensed-usage Growth because it persists a new Job; the command mutation declaration does not apply to enqueue publications.

[MoltaroFunction("recalculate-credit", Name = "Recalculate credit",
Description = "Recomputes the customer credit snapshot.")]
[MoltaroCommand(PermissionKey = "acme.commands.recalculate",
LicenseUsageMutation = MoltaroLicenseUsageMutationKindEnum.Neutral,
TimeoutSeconds = 120)]
public sealed class RecalculateCredit : CommandFunction<RecalculateResult>
{
public override async Task<CommandResult<RecalculateResult>> OnRunAsync(
CancellationToken cancellationToken)
{
// ... business logic ...
return CommandResult<RecalculateResult>.Ok(result);
}
}

A message passed to CommandResult.Failed(...) is returned to the caller as user-facing text: keep it actionable and free of secrets or internal diagnostics. Use CommandResult.ValidationFailed(...) to reject an invocation with field-level validation issues instead of a generic failure.

For choosing between C# and declarative logic, project layout, and the build and apply workflow, continue with C# business logic.

Tasks provides two supported paths. Prefer ITaskInstanceAutomationCommandService and ITaskInstanceAutomationQueryService from Moltaro.Package.NET.Tasks.Automation for application work. They share the Tasks interface’s authorization, capacity, Parts, numbering, history, concurrency, events and replay. The Net Operation Project includes the services in generated dependency injection. See Tasks runtime automation for asynchronous assignment, signals and agent rules.

The direct EF path uses public MoltaroOperationEntity classes in Moltaro.Package.NET.Tasks.Schema. GeneratedWorkspaceContext.TaskInstances and its configuration/history/interval DbSets are writable. Direct SaveChanges does not invoke operational commands: the author owns consistency, evidence and side effects. Database constraints remain active. Application commands commit in a separate scope and do not join an open authored EF transaction.

Static relationships include scalar foreign keys and typed EF navigations. For example, query through task.TaskDefinition.Key, load task.CreationRecord.CommandRecord with Include/ThenInclude, or filter task.StateChanges in a LINQ projection. TaskInstance exposes optional CreationRecord/CompletionRecord and collections of assignments, state changes, commands and execution/interruption intervals. Queue work also exposes its current TaskQueue and its QueueResidences; the open residence records when the Task entered its current Queue. Navigation loading is explicit. Reason and Outcome navigations refer to current configuration; saved historical names, versions and comments remain on the evidence records.

InputDataBinding and WorkingDataBinding refer to ownership metadata. The actual Input/Working records have Definition-specific generated Entity types and are queried through their own DbSets. These relationships are also available in the generated workspace context.

using Moltaro.Package.NET.Functions;
using Moltaro.Package.NET.Tasks.Automation;
using Moltaro.Package.NET.Tasks.Instances;
public sealed record CreateTaskArgs(string OperationId, string DefinitionId, string WorkerId);
[MoltaroFunction("tasks.createDirect", Key = "tasks.createDirect", Name = "Create Direct work")]
public sealed class CreateDirectWork(ITaskInstanceAutomationCommandService commands)
: GlobalFunction<CreateTaskArgs>
{
public override async Task<FunctionResult> OnRunAsync(CreateTaskArgs args, CancellationToken token)
{
var result = await commands.CreateDirectAsync(new CreateDirectTaskRequest(
args.OperationId, args.DefinitionId, args.WorkerId, Subject: "Scheduled work"), token);
result.ThrowIfFailed();
return FunctionResult.OkData(new { result.Data!.Replayed, TaskId = result.Data.Result?.Id });
}
}

Choose an active Direct Definition and a real admitted Worker. Creation does not Start execution. For Direct work, services also support assignment, Start/Stop, Pause/Resume, Return to Idle, Block/Resolve, timed Postpone with early return, and Complete/Cancel; Queue work is described below. Read the current RowVersion before a new command. Preserve OperationId for exact retries after lost responses; a new intention needs a new key. Success may omit the result if View is absent.

CreateDirectTaskRequest also accepts Description, DueAtUtc, Context, Input and Working field values. The Definition’s due-date mode, Context admission, the caller’s own Context access and the Working edit rule for Idle decide whether they are accepted; Entity field errors name Input.<key> or Working.<key>. Omit a Part key to receive the field’s configured default; send an explicit null to leave it empty. The two payloads are different intentions under one OperationId, and a Working section that contains only null values still needs the Working grant. Existing work is changed through property and Part commands:

CommandRequestDefinition grant
EditDetailsAsyncTaskInstanceDetailsRequest (Subject, Description)EditDetails
ChangeDueDateAsyncTaskInstanceDueDateRequest (UTC instant or null)ChangeDueDate and due-date mode
ChangePriorityAsyncTaskInstancePriorityRequest (Priority, SubPriority)ChangePriority
ChangeContextAsyncTaskInstanceContextRequest (admitted identity or null)ChangeContext and target access
EditInputDataAsyncTaskInstanceDataRequest (Task and Part RowVersion, field patch)EditInputData
EditWorkingDataAsyncTaskInstanceDataRequest (Task and Part RowVersion, field patch)EditWorkingData

Every request carries the Task RowVersion; a Part patch also carries the Part’s RowVersion from GetDataAsync or GetEditOptionsAsync. Omitted Part keys stay unchanged and an explicit null clears a value. A Part request sent with the current Task and Part versions in which every value equals the stored value is a no-op: no new Task or Part version, evidence or event. The exception is a sent field that triggers a before-save change of the Part: the request then goes through the Part’s record update, which creates a new Part version even when no value changes. The Task gets a new version, a history entry and an event only when a Part value actually changed, and then records only those keys; otherwise keep the Task RowVersion and re-read the Part version before the next Part command. A request with any change creates a new Part version and records only the changed keys. Both versions are part of the command identity, so a retry must repeat them exactly. Queries GetDataAsync (one Part’s readable values and version; a Kind outside Input/Working is a validation failure), GetInstructionsAsync (current Definition instructions and file references, read through Task authority) and GetEditOptionsAsync (the property/Part actions permitted now) complete the surface. ListAsync accepts an optional due window DueFromUtc/DueToUtc: the lower bound is inclusive, the upper bound exclusive, so a calendar day ends at the next local midnight. History entries of these commands carry Changes: one entry per changed key with its ValueKind and typed previous and new values (Text, Integer, AtUtc, or Context with a label the caller may see); a Part key carries only its label, never a value. Direct EF readers find the same facts as TaskPropertyChangeValue rows and decode them with TaskPropertyChangeValueCodec. A Subject is one line: a line break or any other control character is rejected.

Before creating a task, GetCreateFormAsync(definitionId) returns its Subject and due-date modes, admitted Context definitions and both complete Entity forms. The forms are detached JsonElement values using the HTTP Entity form schema, including field options, defaults and validation rules. The typed WorkingCapability describes the default participants. Call GetCreateWorkingCapabilityAsync after choosing different Owner or Responsible participants; the create command checks the grant again.

GetCreateContextCandidatesAsync searches readable targets of an admitted Context definition before creation. GetContextCandidatesAsync does the same under an existing task’s Context edit grant. Both queries take a bounded Take and a Skip that is a multiple of Take; their result includes the exact total. GetEditOptionsAsync includes SubjectMode, due-date mode and the complete Context policy. GetCommandOptionsAsync returns the lifecycle actions available now and, for Queue work, take, take-and-start and assign; each action has an enabled flag and a reason code. Property and Part actions belong to GetEditOptionsAsync.

Priority and SubPriority remain long in C#. Their JSON representations are decimal strings, preserving every signed 64-bit value; numeric input is also accepted. Priority sorting uses Priority, SubPriority and task identity in the requested direction. Instruction metadata is available through the SDK; file bytes use the authenticated Task instruction-file HTTP endpoint, which checks current Task access, Definition binding and file readiness.

using System.Text.Json;
using Moltaro.Package.NET.Functions;
using Moltaro.Package.NET.Tasks.Automation;
using Moltaro.Package.NET.Tasks.Instances;
public sealed record RecordMeasurementArgs(string OperationId, string TaskId, long MeasuredQuantity);
[MoltaroFunction("tasks.recordMeasurement", Key = "tasks.recordMeasurement", Name = "Record a measurement")]
public sealed class RecordMeasurement(ITaskInstanceAutomationCommandService commands, ITaskInstanceAutomationQueryService queries)
: GlobalFunction<RecordMeasurementArgs>
{
public override async Task<FunctionResult> OnRunAsync(RecordMeasurementArgs args, CancellationToken token)
{
var options = await queries.GetEditOptionsAsync(args.TaskId, token);
options.ThrowIfFailed();
var fields = new Dictionary<string, JsonElement>
{
["MeasuredQuantity"] = JsonSerializer.SerializeToElement(args.MeasuredQuantity, new JsonSerializerOptions())
};
var result = await commands.EditWorkingDataAsync(new TaskInstanceDataRequest(
args.OperationId, args.TaskId, options.Data!.RowVersion, options.Data.WorkingRowVersion, fields), token);
result.ThrowIfFailed();
return FunctionResult.OkData(new { result.Data!.Replayed, RowVersion = result.Data.Result?.RowVersion });
}
}

The Net Operation Project supports instance services in Action, Command, HttpEndpoint, TriggerHandler and Job functions. CRON and triggers carry host-authenticated system provenance, without a fabricated user/Worker. Human invocations retain ordinary permissions; an actor string cannot grant authority. Validation and BeforeSaveMutation cannot call application services.

Queue work uses the same services, RowVersion and OperationId rules. CreateQueueAsync takes a CreateQueueTaskRequest with OperationId, DefinitionId, QueueId, an optional managing Responsible, Owner and Subject, and the same Description, due date, Context and Part values as Direct creation. The Queue must be Active and admit the active Queue-mode Definition; the creator does not need to belong to the Queue’s Worker pool. The new Task is Idle, has no Executor and enters the Queue immediately. Depending on the Definition, the managing Responsible is not used, optional or required at creation; this person manages the work but never performs it. Sending an active Queue-mode Definition to CreateDirectAsync fails with tasks.runtime.queue.required and a QueueId field error.

MemberPurpose
ListQueuesAsyncQueues the caller can discover for operational work, with the number of unfinished Tasks the caller can see, CanAcquire and CanDispatch. It is not the configuration catalog and grants no Task access.
ListAsync with QueueIdScope = Visible (the default) lists the caller’s visible Tasks of that Queue. Scope = Available lists the caller’s current acquisition candidates: every eligible Task under Any, only the first or last one under First or Last. Candidates are fixed before search, filters, count and paging. Sort Queue follows the Queue’s configured order; QueueEntry sorts by the time the Task entered the Queue.
TakeAsyncThe calling Worker takes the exact Task named by TaskInstanceTakeRequest (QueueId, TaskId, RowVersion) and becomes its Executor. The Task stays Idle.
TakeAndStartAsyncTakes and starts the Task in one transaction; Start permissions and execution limits are checked together with the assignment.
AssignExecutorAsyncA dispatcher of the Task’s Queue or a Task administrator of its Definition assigns, changes or clears (ClearAssignment) the Executor of any unfinished Task with TaskInstanceQueueAssignmentRequest. Running work stops and a pause or postponement ends, so the Task arrives Idle; a block stays; nothing starts. Changing or clearing an assigned Executor requires Reason (since 1.22.0, also for Idle work).
ReturnToQueueAsyncThe Executor gives the Task back where the Queue’s return policy is AllowedWithReason; Comment is the required reason. The Task waits Idle and unassigned at its place in the Queue; a block stays. Under the default NotAllowed policy the call fails with tasks.runtime.queue.returnNotAllowed.
AssignResponsibleAsync, AssignOwnerAsyncChange the managing Responsible or Owner of Queue work; the Executor is unchanged.
PostponeAsyncPostpones Idle, InProgress or Paused work of an assigned performer for the Definition’s fixed delay with a Postponed reason. It keeps the Executor and the Queue position and uses one of the Definition’s lifetime attempts; Worker Profile and Queue postponed limits apply. PostponedUntilUtc on the snapshot is the return time, and GetCommandOptionsAsync returns the Postponement allowance. The platform returns the Task to Idle when the time comes.
ReturnFromPostponementAsync, ReturnFromPostponementAndStartAsyncEnd a postponement early, to Idle or straight into work under the ordinary Start checks. Anyone but the performer, automation included, must give a Comment as the reason; ReasonRequired on the action in GetCommandOptionsAsync says so in advance.
GetQueueWorkloadAsyncThe current load of the Queue’s pool Workers that the caller can see, across all their work: counts against Worker Profile limits and Overloaded, never the Tasks behind them. Only the Queue’s dispatchers and Task administrators of a Definition it admits receive it, as CanViewWorkload in ListQueuesAsync shows; anyone else gets a not-found failure.

Take and Take-and-Start are self-service only. The caller must be a Worker in the Queue’s pool with free assigned-Task capacity; a Worker outside the pool who can see the Task gets a Forbidden failure, and Jobs or triggers, which run without a Worker, cannot take work. If the Task is no longer the current candidate or its RowVersion changed, the command fails with a conflict and never takes a different Task. A dispatcher can exceed a recipient’s assigned-Task limit only with CapacityOverride, a non-empty Reason and a Worker Profile that allows dispatcher exceptions, and never for their own Worker; a Task administrator who is not a dispatcher cannot use one. The exception is recorded in history as TaskInstanceCapacityException. Since contract 1.22.0 a call that changes or clears an assigned Executor without a Reason fails with tasks.runtime.executor.reasonRequired on field Reason, also for Idle work that earlier versions accepted; give the reason when you reassign or unassign. A Worker who returned work may no longer see it, so the result of ReturnToQueueAsync can be null; repeating the call with the same OperationId still replays it.

An Active Queue accepts new Tasks. A Draining Queue accepts no new Tasks but still serves the Tasks it holds, and an Inactive Queue serves none; a Queue cannot be made Inactive while it still has unfinished Tasks. Queue work supports Start, Stop, Complete and Cancel under the Definition’s permissions, with the Executor as performer. Snapshots add Mode, TaskQueueId, ManagingResponsibleWorkerId, ExecutorWorkerId, QueueEnteredAtUtc and QueueManualRank; ResponsibleWorkerId is the Direct Responsible or the Queue managing Responsible. The Executor pauses, resumes, returns to Idle, blocks and postpones Queue work like a Direct Responsible. Under the default permissions the Queue’s dispatchers and managing Responsible can also block the work and resolve any block; the Executor resolves only a block they reported.

Not available in this version: Auto-assignment and Assignment on request, reassigning executing, paused, blocked or postponed Queue work, Queue return, handoff and transfer, and comments and files on Queue work.

Tasks configuration automation describes the separate Definition, Queue and Worker Profile services and their permissions.