Node Options
Configure each agent node independently by passing a lambda when adding it:
.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:
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:
InputTemplate— if set, substitutesplaceholders using all available upstream valuesInputKey— if set, reads that key from upstream outputsshared.input— the original workflow input, always available to every node- Semantic keys from upstream outputs — checked in order:
"question","input","message","query","prompt" OriginalInput— falls back to the original workflow input string- First available string value in upstream outputs
Timeout & Retry
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": "..." }
node.OutputMode = AgentOutputMode.String;Structured
Parse the response into a typed object. Each property becomes a separate output key.
node.StructuredOutput<AnalysisResult>();
// Outputs: { "sentiment": "positive", "score": 0.9, ... }Union
Agent returns one of several types. Used with type-based routing.
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.
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:
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:
node.MaxConcurrentExecutions = 3; // At most 3 parallel executionsIn JSON config, set maxConcurrent on the node:
"researcher": {
"agent": { ... },
"maxConcurrent": 3
}Error Handling
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 insteadHuman-in-the-Loop Approval
Pause the workflow after a node executes and wait for user approval before downstream nodes run:
// 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:
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:
WorkflowEventCoordinatorimplementsIDisposable. Always dispose it when done, or wrap in ausingblock.
Timeout behavior
Configure what happens if nobody responds:
node.RequiresApproval(new ApprovalConfig
{
Condition = _ => true,
Message = _ => "Approve?",
Timeout = TimeSpan.FromMinutes(10),
TimeoutBehavior = ApprovalTimeoutBehavior.Deny // default
// TimeoutBehavior = ApprovalTimeoutBehavior.AutoApprove
// TimeoutBehavior = ApprovalTimeoutBehavior.SuspendIndefinitely
})| Behavior | Description |
|---|---|
Deny | After Timeout elapses, automatically denies the request and continues with denial logic. This is the default. |
AutoApprove | After Timeout elapses, automatically approves and lets downstream nodes proceed. |
SuspendIndefinitely | Ignores 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:
node.WithContext<SearchMetadata>("SearchTools", new SearchMetadata
{
Provider = "Tavily",
HasAdvancedFeatures = true
})