Portal Community

Config Schema

{
  "nodeType": "UserForm",
  "config": {
    "formId": "form-employee-expense-request",
    "actorExpression": "$output.fetchEmployee.managerId",
    "title": "Expense Approval Request",
    "description": "Please review and submit the expense claim for {{$output.fetchEmployee.name}}",
    "inputMap": {
      "employeeId": "$output.fetchEmployee.employeeId",
      "claimAmount": "$output.parseClaim.amount",
      "currency": "$output.parseClaim.currency",
      "category": "$output.parseClaim.category"
    },
    "submitPort": "submitted",
    "cancelPort": "cancelled",
    "timeoutSeconds": 86400,
    "timeoutPort": "timeout",
    "taskPriority": "high"
  }
}

Config Fields

FieldTypeRequiredDescription
formIdstringYesAtlas Forms form ID to render
actorExpressionexpressionYesUser ID or array of user IDs who can complete this task
titlestring/expressionNoTask title shown in WorkDesk inbox
descriptionstring/expressionNoTask description with expression interpolation
inputMapobjectNoForm field key → expression for pre-population
submitPortstringNoPort key on submit (default: "main")
cancelPortstringNoPort key on cancel (default: "cancelled")
timeoutSecondsnumberNoAuto-cancel if not submitted within time limit

Executor Pattern

[NodeCapability(CapabilityType.Form)]
public class UserFormExecutor : BaseNodeExecutor
{
    public override async Task<NodeExecutionResult> ExecuteAsync(NodeExecutionContext ctx)
    {
        var config = ctx.GetConfig<UserFormConfig>();
        var actorId = ctx.EvaluateExpression<string>(config.ActorExpression);
        var initialValues = config.InputMap.ToDictionary(
            kv => kv.Key,
            kv => ctx.EvaluateExpression<object>(kv.Value));

        // Suspend execution via HIL mechanism
        await _hilService.SuspendAsync(new HILSuspensionRequest
        {
            ExecutionId = ctx.ExecutionId,
            NodeId = ctx.NodeId,
            ActorId = actorId,
            FormId = config.FormId,
            InitialValues = initialValues,
            TaskTitle = ctx.EvaluateExpression<string>(config.Title),
            TimeoutSeconds = config.TimeoutSeconds
        });

        // ExecuteAsync returns null here — the node is now suspended
        // It will be called again when resume() is triggered
        return NodeExecutionResult.Suspended();
    }
}
Resume flow: When the user submits the form, the HIL resume API is called. The engine re-invokes ExecuteAsync with the resumed context, which contains the form response data in ctx.ResumeData. The executor then returns Success(responseData, portKey: config.SubmitPort).