Skip to content

Node Options

Configure each agent node independently by passing a lambda when adding it:

csharp
.AddAgent("researcher", researcherConfig, node =>
{
    node.WithTimeout(TimeSpan.FromSeconds(30));
    node.WithRetry(maxAttempts: 3);
    node.WithInputKey("topic");
    node.WithOutputKey("research");
    node.WithInstructions("Focus on peer-reviewed sources.");
})

Input & Output

By default, a node reads the string output of the previous node and writes to "answer". You can control both:

csharp
node.WithInputKey("topic")          // Read "topic" from upstream outputs
node.WithOutputKey("research")      // Write output under "research"

// Combine multiple upstream values with a {{key}} substitution template
node.WithInputTemplate("Summarize: {{research}}\n\nFact check: {{facts}}")

// Append extra instructions to this node's agent without changing its base config
node.WithInstructions("Focus on peer-reviewed sources only.")

WithInstructions appends to the agent's existing system instructions at execution time. It does not replace them — use it to inject node-specific guidance into an otherwise shared agent config.

Auto-resolution: If InputKey is not set, the node resolves input in this priority order:

  1. InputTemplate — if set, substitutes placeholders using all available upstream values
  2. InputKey — if set, reads that key from upstream outputs
  3. shared.input — the original workflow input, always available to every node
  4. Semantic keys from upstream outputs — checked in order: "question", "input", "message", "query", "prompt"
  5. OriginalInput — falls back to the original workflow input string
  6. First available string value in upstream outputs

Timeout & Retry

csharp
node.WithTimeout(TimeSpan.FromSeconds(30))

// Exponential backoff (default)
node.WithRetry(maxAttempts: 3)
node.WithRetry(maxAttempts: 3, strategy: BackoffStrategy.Linear)

// Retry only on transient errors (timeout, HTTP)
node.WithRetryTransient(maxAttempts: 3)

// Full control
node.WithRetry(new RetryPolicy
{
    MaxAttempts = 5,
    InitialDelay = TimeSpan.FromSeconds(2),
    Strategy = BackoffStrategy.Exponential,
    MaxDelay = TimeSpan.FromSeconds(30)
})

Output Modes

Control how a node's response is structured before being passed downstream:

String (default)

Agent's full text response → { "answer": "..." }

csharp
node.OutputMode = AgentOutputMode.String;

Structured

Parse the response into a typed object. Each property becomes a separate output key.

csharp
node.StructuredOutput<AnalysisResult>();
// Outputs: { "sentiment": "positive", "score": 0.9, ... }

Union

Agent returns one of several types. Used with type-based routing.

csharp
node.UnionOutput<SimpleAnswer, DetailedReport>();
// Outputs: { "matched_type": "SimpleAnswer", "result": {...} }

Supports up to 5 union types: UnionOutput<T1, T2, T3, T4, T5>().

Handoff

Agent calls a handoff_to_{targetId}() function to decide routing.

csharp
node.WithHandoff("billing", "Route to billing for payment questions");
node.WithHandoff("support", "Route to support for technical issues");
// Outputs: { "handoff_target": "billing" }

Or set multiple at once:

csharp
node.WithHandoffs(
    ("billing", "Payment and invoice questions"),
    ("support", "Technical and product questions")
);

Concurrency

Limit how many parallel instances of a node run simultaneously in a fan-out:

csharp
node.MaxConcurrentExecutions = 3;   // At most 3 parallel executions

In JSON config, set maxConcurrent on the node:

json
"researcher": {
  "agent": { ... },
  "maxConcurrent": 3
}

Error Handling

csharp
node.OnErrorStop()                      // Fail the whole workflow (default)
node.OnErrorSkip()                      // Skip this node, continue downstream
node.OnErrorIsolate()                   // Continue with partial data
node.OnErrorFallback("fallbackAgent")   // Use a different node instead

Human-in-the-Loop Approval

Pause the workflow after a node executes and wait for user approval before downstream nodes run:

csharp
// Always require approval
node.RequiresApproval("Approve this output?")

// Only when a condition is met
node.RequiresApproval(
    when: ctx => ctx.HasOutput("delete_action"),
    message: ctx => $"Agent wants to delete: {ctx.GetOutput<string>("delete_action")}"
)

// When a specific field equals a value
node.RequiresApprovalWhen(field: "action", value: "delete")

// When a specific field exists
node.RequiresApprovalWhenExists(field: "irreversible_operation")

Handling the approval event

Create a WorkflowEventCoordinator and pass it to ExecuteStreamingAsync. Call Approve or Deny on it while iterating the stream:

csharp
using HPD.MultiAgent;

var coordinator = new WorkflowEventCoordinator();

await foreach (var evt in workflow.ExecuteStreamingAsync(input, coordinator))
{
    if (evt is NodeApprovalRequestEvent approval)
    {
        Console.Write($"{approval.Message} (y/n): ");
        if (Console.ReadLine() == "y")
            coordinator.Approve(approval.RequestId);
        else
            coordinator.Deny(approval.RequestId, reason: "User rejected");
    }
}

coordinator.Dispose();

Note: WorkflowEventCoordinator implements IDisposable. Always dispose it when done, or wrap in a using block.

Timeout behavior

Configure what happens if nobody responds:

csharp
node.RequiresApproval(new ApprovalConfig
{
    Condition = _ => true,
    Message = _ => "Approve?",
    Timeout = TimeSpan.FromMinutes(10),
    TimeoutBehavior = ApprovalTimeoutBehavior.Deny          // default
    // TimeoutBehavior = ApprovalTimeoutBehavior.AutoApprove
    // TimeoutBehavior = ApprovalTimeoutBehavior.SuspendIndefinitely
})
BehaviorDescription
DenyAfter Timeout elapses, automatically denies the request and continues with denial logic. This is the default.
AutoApproveAfter Timeout elapses, automatically approves and lets downstream nodes proceed.
SuspendIndefinitelyIgnores Timeout entirely — the workflow holds forever until coordinator.Approve() or coordinator.Deny() is called. Use with care in production; the workflow will never time out on its own.

Injecting Context at Runtime

Inject toolkit metadata into a specific node:

csharp
node.WithContext<SearchMetadata>("SearchTools", new SearchMetadata
{
    Provider = "Tavily",
    HasAdvancedFeatures = true
})

Released under the MIT License.