How we built reliable request/response communication with Revit using Named
Pipes</strong >
The Challenge
Building automation around Revit presents a unique problem:
How do you reliably communicate with a running Revit process?
Revit runs as a desktop application with strict threading requirements. The
Revit API must be called from specific contexts, and any automation needs to
respect the ExternalEvent pattern. This creates challenges:
- Asynchronous by nature – You can’t just “call” Revit
synchronously - Threading constraints – Revit API is single-threaded
- Response timing – Need to know when Revit finishes
processing - Data exchange – How to pass complex requests and get
results back
We needed a communication pattern that worked with Revit’s
architecture, not against it.
The Solution: Named Pipes for IPC
After evaluating several approaches, we settled on
Named Pipes – a Windows inter-process communication (IPC)
mechanism that’s perfect for local service-to-service communication.
Why Named Pipes?
We considered several options:
- File watching – High latency, polling overhead, race
conditions - HTTP API inside Revit – Complex threading, stability
concerns - COM/RPC – Heavy, complex registration requirements
- Named Pipes ← Our choice: Fast, reliable, built into
Windows
Named Pipes give us:
- Sub-second communication between processes
- Request/response pattern – Clean, predictable flow
- Binary serialization – Efficient data transfer
- Windows security – ACL-based access control
- No network exposure – Stays local to the machine
The Communication Pattern
Our implementation follows a clean request/response model:
1. The Request Model
public class PipeRequest
{
public string RequestId { get; set; } // Unique identifier
public string Action { get; set; } // "RevitToJson" or "JsonToRevit"
public string InputFilePath { get; set; } // Absolute path to input file
public string OutputFolderPath { get; set; } // Where to write results
public Dictionary<string, string> Parameters { get; set; } // Optional params
}
Simple, focused, and contains everything Revit needs to process the job.
2. The Response Model
public class PipeResponse
{
public string RequestId { get; set; } // Matches the request
public bool Success { get; set; } // Did it work?
public string Message { get; set; } // Human-readable result
public Dictionary<string, string> OutputFiles { get; set; } // Result file paths
}
Clean separation: request contains instructions,
response contains results.
Implementation: Client Side (Queue Service)
The client establishes a connection and sends requests to Revit:
public class RevitPipeClient
{
private readonly string _pipeName = "RevitAutomationPipe";
public async Task<PipeResponse> SendRequestAsync(PipeRequest request)
{
using var pipeClient = new NamedPipeClientStream(
".",
_pipeName,
PipeDirection.InOut,
PipeOptions.Asynchronous
);
// Connect with timeout
await pipeClient.ConnectAsync(5000);
// Serialize and send request
var requestJson = JsonConvert.SerializeObject(request);
var requestBytes = Encoding.UTF8.GetBytes(requestJson);
await pipeClient.WriteAsync(BitConverter.GetBytes(requestBytes.Length));
await pipeClient.WriteAsync(requestBytes);
await pipeClient.FlushAsync();
// Read response
var lengthBytes = new byte[4];
await pipeClient.ReadAsync(lengthBytes, 0, 4);
var responseLength = BitConverter.ToInt32(lengthBytes, 0);
var responseBytes = new byte[responseLength];
await pipeClient.ReadAsync(responseBytes, 0, responseLength);
var responseJson = Encoding.UTF8.GetString(responseBytes);
return JsonConvert.DeserializeObject<PipeResponse>(responseJson);
}
}
Key points:
- Length-prefixed messages (4-byte int + payload)
- JSON serialization for readability
- Async/await for non-blocking I/O
- Clean timeout handling
Implementation: Server Side (Revit Add-in)
The Revit add-in listens on the pipe and processes requests:
public class PipeListener
{
private readonly ExternalEvent _externalEvent;
private readonly JobHandler _jobHandler;
private CancellationTokenSource _cts;
public async Task StartListeningAsync()
{
_cts = new CancellationTokenSource();
while (!_cts.Token.IsCancellationRequested)
{
using var pipeServer = new NamedPipeServerStream(
"RevitAutomationPipe",
PipeDirection.InOut,
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous
);
await pipeServer.WaitForConnectionAsync(_cts.Token);
try
{
// Read request
var lengthBytes = new byte[4];
await pipeServer.ReadAsync(lengthBytes, 0, 4);
var requestLength = BitConverter.ToInt32(lengthBytes, 0);
var requestBytes = new byte[requestLength];
await pipeServer.ReadAsync(requestBytes, 0, requestLength);
var requestJson = Encoding.UTF8.GetString(requestBytes);
var request = JsonConvert.DeserializeObject<PipeRequest>(requestJson);
// Process via ExternalEvent (Revit's threading model)
_jobHandler.SetCurrentJob(request);
_externalEvent.Raise();
// Wait for completion
var response = await _jobHandler.WaitForCompletionAsync();
// Send response
var responseJson = JsonConvert.SerializeObject(response);
var responseBytes = Encoding.UTF8.GetBytes(responseJson);
await pipeServer.WriteAsync(BitConverter.GetBytes(responseBytes.Length));
await pipeServer.WriteAsync(responseBytes);
await pipeServer.FlushAsync();
}
catch (Exception ex)
{
// Send error response
var errorResponse = new PipeResponse
{
RequestId = "unknown",
Success = false,
Message = $"Error: {ex.Message}"
};
var errorJson = JsonConvert.SerializeObject(errorResponse);
var errorBytes = Encoding.UTF8.GetBytes(errorJson);
await pipeServer.WriteAsync(BitConverter.GetBytes(errorBytes.Length));
await pipeServer.WriteAsync(errorBytes);
}
}
}
}
Key points:
- Continuous listening loop
- Single connection at a time (simple, reliable)
- ExternalEvent for Revit API access
- Error handling with structured responses
The ExternalEvent Bridge
Revit’s API requires calls from the main UI thread. ExternalEvent provides the
bridge:
public class JobHandler : IExternalEventHandler
{
private PipeRequest _currentRequest;
private TaskCompletionSource<PipeResponse> _completionSource;
public void SetCurrentJob(PipeRequest request)
{
_currentRequest = request;
_completionSource = new TaskCompletionSource<PipeResponse>();
}
public async Task<PipeResponse> WaitForCompletionAsync()
{
return await _completionSource.Task;
}
public void Execute(UIApplication app)
{
try
{
var response = _currentRequest.Action switch
{
"RevitToJson" => HandleRevitToJson(app, _currentRequest),
"JsonToRevit" => HandleJsonToRevit(app, _currentRequest),
_ => new PipeResponse
{
Success = false,
Message = $"Unknown action: {_currentRequest.Action}"
}
};
response.RequestId = _currentRequest.RequestId;
_completionSource.SetResult(response);
}
catch (Exception ex)
{
_completionSource.SetResult(new PipeResponse
{
RequestId = _currentRequest.RequestId,
Success = false,
Message = ex.Message
});
}
}
public string GetName() => "RevitAutomationJobHandler";
}
This pattern elegantly handles Revit’s threading requirements while
maintaining async communication.
Real-World Usage Example
From the queue service perspective, using this is simple:
var pipeClient = new RevitPipeClient();
var request = new PipeRequest
{
RequestId = Guid.NewGuid().ToString(),
Action = "RevitToJson",
InputFilePath = @"C:Jobsbuilding.rvt",
OutputFolderPath = @"C:Jobsoutput",
Parameters = new Dictionary<string, string>
{
{ "includeGeometry", "true" },
{ "exportFormat", "json" }
}
};
var response = await pipeClient.SendRequestAsync(request);
if (response.Success)
{
Console.WriteLine($"Success! Output: {response.OutputFiles["output.json"]}");
}
else
{
Console.WriteLine($"Failed: {response.Message}");
}
Total round-trip time: ~5 seconds for typical Revit
processing.
Benefits of This Approach
1. Simplicity
No message brokers, no queues, no complex infrastructure. Just two processes
talking directly.
2. Performance
Named Pipes are extremely fast for local communication. Sub-millisecond
connection times, minimal serialization overhead.
3. Reliability
Windows Named Pipes are battle-tested. They handle process crashes gracefully
– the pipe closes, client gets notification.
4. Security
Pipes stay local to the machine. No network sockets, no firewall rules, no TLS
certificates to manage.
5. Debugging
JSON serialization makes debugging trivial. Log the request/response JSON and
you have complete visibility.
6. Scalability
Want to scale? Run multiple Revit instances on separate machines with their
own pipe names. Simple load distribution.
Technical Lessons Learned
1. Length-Prefixed Messages Are Essential
Without knowing message length upfront, you can’t reliably read from a stream.
The 4-byte length prefix pattern is simple and bulletproof:
// Write: [4 bytes: length][N bytes: payload]
await stream.WriteAsync(BitConverter.GetBytes(payload.Length));
await stream.WriteAsync(payload);
// Read: [4 bytes: length][N bytes: payload]
var lengthBytes = new byte[4];
await stream.ReadAsync(lengthBytes, 0, 4);
var length = BitConverter.ToInt32(lengthBytes, 0);
var payload = new byte[length];
await stream.ReadAsync(payload, 0, length);
2. ExternalEvent Is Your Friend
Don’t fight Revit’s threading model. Use ExternalEvent +
TaskCompletionSource to bridge async pipe communication with
Revit’s synchronous API:
// Set up the job
_completionSource = new TaskCompletionSource<PipeResponse>();
_externalEvent.Raise();
// Wait asynchronously
var response = await _completionSource.Task;
// In Execute() method (called on Revit's main thread):
var result = ProcessRevitFile(); // Revit API calls
_completionSource.SetResult(result); // Unblock awaiter
3. Single Connection at a Time
We experimented with
NamedPipeServerStream.MaxNumberOfServerInstances = -1
(unlimited). It added complexity without benefit.
Keep it simple: One connection at a time. If multiple clients
need access, queue them at a higher level.
4. Timeouts Are Critical
Always set connection timeouts. A crashed client shouldn’t block the server
forever:
await pipeClient.ConnectAsync(5000); // 5-second timeout
On the server side, use CancellationToken for graceful shutdown:
await pipeServer.WaitForConnectionAsync(cancellationToken);
5. JSON vs Binary Serialization
We chose JSON for debuggability. The performance difference is negligible for
our use case (typical messages are < 10KB).
If you’re transferring large geometry data, consider:
- File paths instead of inline data
- MessagePack or Protocol Buffers for binary efficiency
- Compression for large JSON payloads
Architecture Diagram
┌─────────────────────────────────────────────────────────┐
│ Queue Service │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ RevitPipeClient │ │
│ │ │ │
│ │ 1. Create PipeRequest │ │
│ │ 2. Connect to "RevitAutomationPipe" │ │
│ │ 3. Send JSON request │ │
│ │ 4. Await response │ │
│ │ 5. Return PipeResponse │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────┬──────────────────────────────────────┘
│
│ Named Pipe
│ "RevitAutomationPipe"
│
┌──────────────────▼──────────────────────────────────────┐
│ Revit Add-in │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ PipeListener │ │
│ │ │ │
│ │ 1. Listen on named pipe │ │
│ │ 2. Receive PipeRequest │ │
│ │ 3. Queue to ExternalEvent │ │
│ │ 4. Execute on Revit thread │ │
│ │ 5. Return PipeResponse │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ JobHandler : IExternalEventHandler │ │
│ │ │ │
│ │ Execute(UIApplication app) │ │
│ │ { │ │
│ │ // Safe Revit API access here │ │
│ │ OpenDocument(inputPath); │ │
│ │ ExportToJson(outputPath); │ │
│ │ return PipeResponse │ │
│ │ } │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
When to Use This Pattern
Perfect for:
- Desktop application automation (Revit, AutoCAD, etc.)
- Local service-to-service communication
- Request/response workflows
- Single-machine deployments
Not ideal for:
- Network communication (use HTTP/gRPC)
- Pub/sub patterns (use message brokers)
- Cross-platform scenarios (Named Pipes are Windows-specific)
That’s it. ~200 lines of code for robust IPC.
The Bottom Line
Named Pipes are an elegant solution for communicating with desktop
applications like Revit.</strong >
Key takeaways:
- Simple to implement (no heavy frameworks needed)
- Fast and reliable for local communication
- Works beautifully with Revit’s threading model
- Production-ready with minimal code
- Easy to debug and maintain
If you’re building automation around Revit, AutoCAD, or any Windows desktop
application, consider Named Pipes before reaching for more complex solutions.
Resources
Technologies Used:
System.IO.Pipes(.NET)Newtonsoft.Jsonfor serialization- Revit API 2024
ExternalEventpattern
Further Reading:
Have you used Named Pipes for IPC? What patterns worked well for you? Share
your experiences in the comments!</em >
👇
Disclaimer- I do not claim to be an expert in this area. We have used this for the first time and thought of sharing. If you have any suggestions which can help, please feel free to share.
Tags: #Revit #NamedPipes #IPC #DotNet #BIM #Automation
#SoftwareArchitecture #CSharp

