Portal Community

HIL Actor Pattern

1
Workflow reaches a HIL node assigned to "OctopusAgent" Flow Studio suspends the workflow and puts the HIL task in the actor queue for "OctopusAgent".
2
IOctopusHILActor polls the ProcessServer task queue Every QueuePollMs milliseconds, the HIL actor polls for new tasks assigned to the configured actor name.
3
Task dispatched to the Octopus agent The HIL task data is serialised as a prompt and sent to the HILActor.AgentId agent as a chat turn.
4
Agent reasons and calls submit_hil_decision tool The agent evaluates the task data, calls its own tools as needed, then calls the submit_hil_decision MCP tool.
5
ProcessServer resumes the workflow with the decision The workflow continues from the HIL node with the agent's decision as the HIL actor response.

IOctopusHILActor Implementation

public class OctopusHILActor : IOctopusHILActor
{
    private readonly IProcessServerClient _processServer;
    private readonly IOctopusAgentEngine  _agentEngine;
    private readonly ProcessPluginConfig  _config;
    private          CancellationTokenSource? _cts;

    public async Task StartPollingAsync(CancellationToken ct)
    {
        _cts = CancellationTokenSource.CreateLinkedTokenSource(ct);

        _ = Task.Run(async () =>
        {
            while (!_cts.Token.IsCancellationRequested)
            {
                try
                {
                    var tasks = await _processServer.GetPendingHILTasksAsync(
                        actorName:  "OctopusAgent",
                        limit:      10,
                        _cts.Token);

                    foreach (var task in tasks)
                        _ = ProcessHILTaskAsync(task, _cts.Token);
                }
                catch (Exception ex) when (ex is not OperationCanceledException)
                {
                    // Log and continue polling
                }

                await Task.Delay(_config.HILActor.QueuePollMs, _cts.Token);
            }
        }, _cts.Token);
    }

    private async Task ProcessHILTaskAsync(HILTask task, CancellationToken ct)
    {
        // Build a prompt describing the HIL task
        var prompt = $"""
            You are reviewing a HIL approval task.
            Task Type:   {task.TaskType}
            Description: {task.Description}
            Data:        {JsonSerializer.Serialize(task.Data, JsonOptions.Indented)}

            Review the task and call submit_hil_decision with:
            - decision: "Approve", "Reject", or "Escalate"
            - reason:   your explanation
            - form_data: any required fields (if this is a form task)
            """;

        await _agentEngine.ChatAsync(new AgentChatRequest
        {
            AgentId   = _config.HILActor.AgentId,
            SessionId = $"hil-{task.TaskId}",
            Message   = prompt,
            TenantId  = task.TenantId
        }, ct);
    }
}

submit_hil_decision Tool Schema

{
  "name": "submit_hil_decision",
  "description": "Submit a decision for a Human-In-the-Loop approval task. " +
                 "Call this after reviewing the task data provided in the system context.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "task_id":   { "type": "string", "description": "The HIL task ID to respond to" },
      "decision":  { "type": "string", "enum": ["Approve", "Reject", "Escalate"] },
      "reason":    { "type": "string", "description": "Explanation of the decision" },
      "form_data": { "type": "object", "description": "Form field responses if required" }
    },
    "required": ["task_id", "decision", "reason"]
  }
}
Automated decisions carry full responsibility. Any decision the agent makes has the same effect as a human decision. Ensure the agent's system prompt, tools, and data access are appropriate for the approval type before enabling automated HIL.