Robovue.Sdk
0.2.0
dotnet add package Robovue.Sdk --version 0.2.0
NuGet\Install-Package Robovue.Sdk -Version 0.2.0
<PackageReference Include="Robovue.Sdk" Version="0.2.0" />
<PackageVersion Include="Robovue.Sdk" Version="0.2.0" />
<PackageReference Include="Robovue.Sdk" />
paket add Robovue.Sdk --version 0.2.0
#r "nuget: Robovue.Sdk, 0.2.0"
#:package Robovue.Sdk@0.2.0
#addin nuget:?package=Robovue.Sdk&version=0.2.0
#tool nuget:?package=Robovue.Sdk&version=0.2.0
Robovue.Sdk
Report your bots' runs to Robovue, which watches every bot in one place and tells you when one fails or doesn't run. This package signs each report, retries when the network or Robovue has a problem, checks a report against Robovue's rules before sending it, and can send dry runs while you set it up.
Install
dotnet add package Robovue.Sdk
Requirements
- .NET Framework 4.6.2 or later, or .NET 6 or later. The package targets netstandard2.0 and net8.0, and is tested on .NET Framework 4.8 and .NET 10.
- A Robovue workspace. Signing up is free.
Can't add a package where your bot runs? Connections → HTTP API in Robovue gives you the same request as plain C#, Python and shell code to paste in, and the wire contract below describes it for any language.
Quickstart
- Sign up at robovue.co.uk.
- Copy your credentials from Settings → Connections → HTTP API: the webhook secret, and
your tenant ID, which is the long id in the address shown there
(
https://app.robovue.co.uk/ingest/<tenant ID>/runs). - Keep them out of your code. Set them as environment variables on the machine that runs the
bot,
ROBOVUE_TENANT_IDandROBOVUE_WEBHOOK_SECRET, or in your platform's credential store, and create one client:
using System.Text.Json;
using Robovue.Sdk;
// One client for the whole process: it's safe to share between threads.
var robovue = new RobovueClient(new RobovueClientOptions
{
BaseAddress = new Uri("https://app.robovue.co.uk"),
TenantId = Guid.Parse(Environment.GetEnvironmentVariable("ROBOVUE_TENANT_ID")!),
WebhookSecret = Environment.GetEnvironmentVariable("ROBOVUE_WEBHOOK_SECRET"),
});
- Wrap your bot's job. Report the run as
Runningwhen it starts, then asSuccessorFailurewhen it ends, with the same delivery id so Robovue knows it's one run:
var deliveryId = Guid.NewGuid().ToString(); // one per run: it ties the run's reports together
var startedAt = DateTimeOffset.UtcNow;
await robovue.TryReportRunAsync(new RunReport
{
BotId = "invoice-bot-01",
Platform = "dotnet",
Status = RunStatus.Running,
StartedAt = startedAt,
}, deliveryId);
try
{
ProcessInvoices(); // your bot's work
}
catch (Exception ex)
{
await robovue.TryReportRunAsync(new RunReport
{
BotId = "invoice-bot-01",
Platform = "dotnet",
Status = RunStatus.Failure,
StartedAt = startedAt,
CompletedAt = DateTimeOffset.UtcNow,
FailureCategory = FailureCategory.ApplicationException,
Payload = JsonSerializer.SerializeToElement(new { error = ex.Message }),
}, deliveryId);
throw;
}
await robovue.TryReportRunAsync(new RunReport
{
BotId = "invoice-bot-01",
Platform = "dotnet",
Status = RunStatus.Success,
StartedAt = startedAt,
CompletedAt = DateTimeOffset.UtcNow,
}, deliveryId);
TryReportRunAsync never throws, so a problem reaching Robovue can't break your bot. The job's
own exception is reported, then rethrown, so the bot still fails the way it always did.
- Check it arrives. Run the bot once with
DryRunon, then for real, and watch the report land in the Verify step on Connections → HTTP API, in the request log, and on Fleet.
Make missed runs alert
A failure alerts as soon as it's reported. A run that never happens can only be noticed if Robovue
knows when to expect it, so in Manage bots, register the bot (its id is BotId) and give it a
schedule. Until a bot is registered, its runs appear under Unregistered ids, and each new id
counts toward your plan's bot limit.
Reporting in more detail
Failure categories
On a Failure, set FailureCategory to say why it failed. It's shown on the run, filterable on
Runs, and named in the alert. Leave it out if you don't know: Robovue records Uncategorized, and a
run can be reclassified later from its page.
var result = await robovue.ReportRunAsync(new RunReport
{
BotId = "invoice-bot-01",
Platform = "dotnet",
Status = RunStatus.Failure,
StartedAt = startedAt,
CompletedAt = DateTimeOffset.UtcNow,
FailureCategory = FailureCategory.BusinessException,
});
| Category | Use it for |
|---|---|
BusinessException |
A business rule stopped the run: bad input data, a missing record, a rejected transaction. |
ApplicationException |
Something broke in the bot or the application it drives. |
Timeout |
The run, or a step in it, took too long. |
AuthenticationError |
The bot couldn't sign in to something it needs. |
Cancelled |
A person or a scheduler stopped the run. |
Uncategorized |
You don't know. The default. |
Attaching context with Payload
Payload takes any JSON and is stored as sent, shown in full on the run's page. Use it for whatever
would help you debug the run later: an invoice number, the step that failed, a stack trace. If it
has an error, errorMessage or message field, the alert uses that text to say what went wrong.
var result = await robovue.ReportRunAsync(new RunReport
{
BotId = "invoice-bot-01",
Platform = "dotnet",
Status = RunStatus.Failure,
StartedAt = startedAt,
CompletedAt = DateTimeOffset.UtcNow,
FailureCategory = FailureCategory.Timeout,
Payload = JsonSerializer.SerializeToElement(new
{
invoiceId = "INV-2291",
error = "Timed out waiting for the vendor portal",
}),
});
The whole report, payload included, must be under 64 KB.
Attaching a screenshot or log to a failed run
On the Team plan and above, you can attach one screenshot or log excerpt to a Failure run once
it's reported. Screenshots are off for each bot until someone turns them on (Manage bots →
Screenshots), because a screenshot can capture credentials or other sensitive data on screen.
await robovue.UploadAttachmentAsync(result.RunId, screenshotBytes, "image/png");
contentType is "image/png", "image/jpeg" or "text/plain", up to 5 MB, one per run. Robovue
deletes attachments after 30 days, or 7 or 14 if an Admin chose that. An upload for a bot with
screenshots off, or on a plan without them, gets the same 404 as an unknown run, so catch it if a
missing screenshot shouldn't fail your bot.
Reporting without ever throwing
Monitoring shouldn't become a new way for a bot to fail. TryReportRunAsync never throws for a
failed report: an invalid report, a rejection or a network problem comes back in the outcome for
you to log. It still honours cancellation of the token you pass.
var outcome = await robovue.TryReportRunAsync(report);
if (!outcome.Succeeded)
Console.Error.WriteLine($"Couldn't report the run to Robovue: {outcome.Error}");
When a report is rejected
ReportRunAsync throws RobovueApiException. Branch on Code, which is stable; the message is for
people. Field names the field at fault when there is one, and RequestId is worth quoting if you
contact support.
try
{
await robovue.ReportRunAsync(report);
}
catch (RobovueApiException ex) when (ex.Code == "timestamp_out_of_range")
{
// This machine's clock is more than 5 minutes out. The message gives Robovue's time.
Console.Error.WriteLine(ex.Message);
}
Network errors and 5xx responses are retried automatically (3 attempts by default), and so is a
429 after its Retry-After, unless that's more than a minute, when the 429 is thrown instead of
blocking your bot. A report is checked against the server's own rules before anything is sent, so
most mistakes fail at once with an ArgumentException. Every code is in the
responses table below, with whether a retry can help.
Checking a new integration with a dry run
With DryRun = true, every request is checked exactly as a real one would be (signature, fields,
plan limits, delivery id) but nothing is stored. The result says what would have happened, and a
request that would fail throws the real error.
using var robovue = new RobovueClient(new RobovueClientOptions
{
BaseAddress = new Uri("https://app.robovue.co.uk"),
TenantId = tenantId,
WebhookSecret = webhookSecret,
DryRun = true, // checked in full, stored nowhere
});
var result = await robovue.ReportRunAsync(report);
Console.WriteLine($"{result.DryRun} {result.WouldBe}"); // True created
Dry runs show in the request log, which warns you if any arrived in the last day, so a bot left in dry-run mode doesn't go unnoticed. Turn it off before going live.
Runs that take a while
The quickstart reports Running first. That's optional, but it means a bot that starts a job and
then hangs shows as Running straight away, rather than only when its next run fails to turn up.
Reusing the delivery id is the whole mechanism: a fresh id on the second report makes two runs,
one stuck Running and one that looks like nobody started it. A run left Running silences
nothing: if it never completes, the bot still goes Late and then Overdue on its schedule.
Using it in your bot
One client per process. RobovueClient holds no state between calls, so share one instance
across threads. Dispose it when the process ends; a client you create yourself also disposes the
HttpClient it made.
Synchronous code. UiPath's Invoke Code, Blue Prism code stages and other places that can't
await can block on the call:
var outcome = robovue.TryReportRunAsync(report).GetAwaiter().GetResult();
Timeouts, proxies and TLS. The client uses .NET's HttpClient defaults: the machine's proxy
settings, the operating system's trusted certificates (so a corporate TLS-inspection certificate
installed on the machine is trusted), and a 100-second timeout per attempt. To change any of them,
pass your own HttpClient, such as one from IHttpClientFactory:
var http = new HttpClient(new HttpClientHandler
{
Proxy = new WebProxy("http://proxy.example.internal:8080"),
})
{
Timeout = TimeSpan.FromSeconds(30),
};
var robovue = new RobovueClient(options, http); // the SDK never disposes a client you pass in
Options
RobovueClientOptions |
Default | What it does |
|---|---|---|
BaseAddress |
required | https://app.robovue.co.uk |
TenantId |
required | Your workspace's tenant ID. |
WebhookSecret |
required | Signs every request. Never put it in source code. |
MaxRetryAttempts |
3 |
Attempts per call, the first included. |
RetryBaseDelay |
200 ms | The first wait between attempts; each later wait doubles. |
DryRun |
false |
Check every request in full and store nothing. |
Troubleshooting
| What you see | What to do |
|---|---|
invalid_signature |
The secret is wrong or was rotated. Copy the current one from Connections → HTTP API. |
timestamp_out_of_range |
The machine's clock is more than 5 minutes out. Sync it (on Windows, w32tm /resync). |
unknown_tenant |
The tenant ID is wrong. Copy it from the address on Connections → HTTP API. |
bot_cap_reached |
A new bot id would go over your plan's bot limit. Delete a bot you no longer use, or change plan. |
payload_too_large |
The report is over 64 KB. Send less in Payload, and attach a log file instead. |
| A 403 with no code | Something between your bot and Robovue blocked the request, such as a proxy rewriting it. Check with whoever runs your network, or send us the time it happened. |
An HttpRequestException |
The machine can't reach app.robovue.co.uk. Check its proxy and firewall. |
| The report succeeds but nothing shows on Fleet | DryRun is still on, the bot is archived (see Manage bots → Archived bots), or the credentials belong to another workspace. |
| A bot stopped running and nobody was told | It has no schedule. See Make missed runs alert. |
The request log, in Connections, shows every request from the last 7 days and the exact reason any was turned away.
The wire contract (any language)
This is exactly what RobovueClient sends. From a language with no Robovue SDK, send the same
request yourself. The tables below are generated from the server's own contract, so they list every
field, header, response and limit, including the optional ones.
POST /ingest/{tenantId}/runs
The endpoint both SDKs call, and the one to use from any other language. A JSON object with the fields below.
Headers
| Header | Value | Description |
|---|---|---|
Content-Type |
application/json | The body is JSON. A charset parameter is fine. |
X-Robovue-Delivery-Id |
1–200 printable ASCII characters, no spaces | Unique to this run. Sending it again — a retry, or the Success/Failure that ends a Running run — updates or confirms the same run instead of creating a second one. A GUID, or your bot's own run id, both work. |
X-Robovue-Timestamp |
Unix time in whole seconds | When you signed the request. It must be within 5 minutes of Robovue's clock either way, so a captured request stops working within minutes. Sign each attempt, retries included, just before sending it. |
X-Robovue-Signature |
v1=<lowercase hex> | HMAC-SHA256, keyed with your webhook secret, of the timestamp, the word run, the delivery id — each followed by a line break — then the exact request body; hex-encoded in lowercase and prefixed with v1=. Sign the exact bytes you send: re-serialising the JSON after signing changes them. Several comma-separated v1= values are accepted, and any one matching is enough. |
X-Robovue-Dry-Run |
true | Checks the request exactly as a real one — authentication, every field, the delivery id, your plan's bot limit — and stores nothing. The response says what would have happened. Use it to test an integration without creating runs. Any other value, or no header, is an ordinary request. |
Body fields
| Field | Type | Required | Description |
|---|---|---|---|
botId |
string, ≤ 200 chars | Yes | Your name for the bot. Every run with the same botId is the same bot on your dashboard. Spaces and punctuation are fine; not / or , no spaces at either end, and no tabs or line breaks. The same id registers and manages the bot through the Management API. |
platform |
string, ≤ 50 chars | Yes | What runs the bot, shown beside it on the dashboard — any short label, such as dotnet, python, go or blue-prism. Same whitespace rules as botId. |
status |
one of Success, Failure, Running |
Yes | How the run went. Running reports a run that has started and not finished yet; send the same delivery id again with Success or Failure when it ends. Values are case-sensitive. |
startedAt |
timestamp | Yes | When the run started, as an RFC 3339 timestamp with a time zone offset — Z for UTC or ±hh:mm, e.g. 2026-07-17T09:00:00Z. Not before 2000, and not more than an hour ahead of Robovue's clock. Unix times aren't accepted. |
completedAt |
timestamp | Required when status is Success or Failure; leave it out when status is Running | When the run finished. Same format and limits as startedAt, and not before it. |
failureCategory |
one of Uncategorized, BusinessException, ApplicationException, Timeout, AuthenticationError, Cancelled |
No — only with status Failure | Why the run failed, if your bot can tell. Leave it out if not — it becomes Uncategorized and can be changed later on the run's page. Values are case-sensitive. |
payload |
object | No | Anything that will help you debug the run later — an invoice number, the step that failed, a stack trace. Must be a JSON object; it's stored as sent and shown on the run's page. A top-level error, errorMessage or message string becomes the summary in the failure alert. Counts toward the 64 KB request limit. |
Responses
Every error body is {"error": "…", "code": "…", "field": "…", "requestId": "…"} — field only when the error is about one field — and every response carries an X-Request-Id header.
| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 201 | — | A new run was recorded. Body: {"runId": "…", "duplicate": false} | No need — nothing more to send |
| 200 | — | The run was already known. duplicate is false when this completed a run reported as Running, and true when it repeated a report already recorded — a retry, which is safe. | No need — nothing more to send |
| 200 | — | A dry run passed every check. Body: {"dryRun": true, "wouldBe": "created"} — or "completed" or "duplicate". Nothing was stored. | No need — nothing more to send |
| 400 | invalid_body |
The body isn't a single valid JSON object. The message gives the line and position. | No — fix the request first |
| 400 | duplicate_field |
A field appears twice. | No — fix the request first |
| 400 | unknown_field |
A field isn't one of the documented names. Names are case-sensitive; put your own data inside payload. | No — fix the request first |
| 400 | missing_field |
A required field is missing, blank or null. field names it. | No — fix the request first |
| 400 | invalid_field |
A field's value breaks its rule. field names it and the message says how. | No — fix the request first |
| 400 | missing_delivery_id |
X-Robovue-Delivery-Id is missing. | No — fix the request first |
| 400 | invalid_delivery_id |
X-Robovue-Delivery-Id is longer than 200 characters, or has spaces or non-ASCII characters. | No — fix the request first |
| 400 | bot_cap_reached |
This would be a new bot and your plan's bot limit is reached. Archive and delete a bot you no longer use, or raise the limit on the Billing page. | No — fix the request first |
| 401 | missing_signature |
X-Robovue-Signature is missing. | No — fix the request first |
| 401 | missing_timestamp |
X-Robovue-Timestamp is missing. | No — fix the request first |
| 401 | invalid_timestamp |
X-Robovue-Timestamp isn't whole Unix seconds — milliseconds are the usual cause. | No — fix the request first |
| 401 | timestamp_out_of_range |
X-Robovue-Timestamp is more than 5 minutes from Robovue's clock. The message gives the server's time; fix the sending machine's clock. | Yes, once re-signed with the current time |
| 401 | invalid_signature |
The signature doesn't match. Check the secret, and that you sign the timestamp, the delivery or run id and the exact bytes you send. | No — fix the request first |
| 404 | unknown_tenant |
No workspace has this tenant id. Copy the URL from your Connect page. | No — fix the request first |
| 409 | delivery_id_conflict |
The delivery id already belongs to a different bot's run. Reuse a delivery id only for the same bot's run. | No — fix the request first |
| 413 | payload_too_large |
The body is over 64 KB. | No — fix the request first |
| 415 | unsupported_media_type |
Content-Type isn't application/json. | No — fix the request first |
| 429 | rate_limited |
Too many requests. Retry-After says how many seconds to wait. | Yes, after the Retry-After seconds |
| 500 | internal_error |
Something went wrong on Robovue's side. Quote the requestId if it keeps happening. | Yes, with backoff — the same delivery id makes it safe |
Limits
| Limit | Value | Notes |
|---|---|---|
| Run report size | 64 KB | Per request, payload included. Anything larger is rejected with 413 — it isn't truncated. |
| Run reports | 600 per minute per workspace | Shared by the run report, Power Automate and UiPath endpoints. A start and its completion count as two. |
| Attachment size | 5 MB | One file per failed run: PNG, JPEG or UTF-8 text. |
| Attachment uploads | 50 per hour per workspace | Across all bots. |
| Attachment storage | 500 MB per workspace | Attachments are deleted after the workspace's retention, 30 days at most, which frees their space. |
| Management API | 300 requests per minute per workspace | Every /tenants/… request. |
| Per address | 3,000 requests per minute per client IP | Across ingestion and the Management API together, for every workspace sending from that address. |
Signing, in one line: "v1=" + hex(HMAC-SHA256(key: webhook secret, message: timestamp + "\nrun\n" + delivery id + "\n" + the exact body bytes)),
lowercase, sent with the same timestamp in X-Robovue-Timestamp (Unix seconds). Sign each attempt, retries
included, just before sending it. Sign the bytes you actually send: re-serialising the JSON after signing (a
different key order, different whitespace) changes them and the request is rejected with invalid_signature.
Connections → HTTP API has a signature checker to compare yours against.
The attachment upload (POST /ingest/{tenantId}/runs/{runId}/attachment) uses the same scheme with the word
attachment in place of run and the run id (lowercase, hyphenated) in place of the delivery id, computed over
the raw file bytes instead of a JSON body. It has no delivery-id header: one attachment per run is what makes it
safe to retry.
This scheme is only for the /ingest/{tenantId}/runs endpoint. UiPath Orchestrator, Power Automate and
Automation Anywhere connect to Robovue without code; Connections in Robovue sets each one up.
Versions and support
- The package follows semantic versioning. The changelog lists what changed in each version.
- For bots written in Python,
robovue-sdkis the same client. - Questions and problems: contact@robovue.co.uk. Quote the
RequestIdfrom the error if there is one. - Found a security problem? See robovue.co.uk/security.
Licence
MIT, so you can use, change and ship this package freely (see LICENSE in the package). Using the Robovue
service is covered by the terms.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Text.Json (>= 8.0.5)
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.0 | 42 | 10/18/2026 |
# Changelog
## 0.2.0 — 2026-10-18
The first public release.
- `ReportRunAsync` reports a run, signed with Robovue's v1 scheme: the signature covers a
timestamp, the endpoint and the delivery id as well as the body, so a captured request can't be
replayed and stops working after five minutes. Every retry is signed afresh.
- `TryReportRunAsync` reports a run without ever throwing, so monitoring can't break the bot. Why a
report failed is in `RunReportOutcome.Error`.
- `RunStatus.Running` and a reused delivery id report a run that's still going, then its outcome.
`RunReportResult.DeliveryId` gives back the id that was sent.
- `UploadAttachmentAsync` attaches a screenshot or log to a failed run.
- `RobovueClientOptions.DryRun` checks every request in full and stores nothing; the result's
`DryRun` and `WouldBe` say what would have happened.
- `RobovueApiException` carries the server's `Code`, `Field` and `RequestId`.
- Network errors and 5xx responses are retried with backoff, and a `429` after its `Retry-After`
(a wait over one minute is thrown instead of blocking the bot).
- Reports are checked against the server's own rules before sending: names, times, categories, the
payload, the 64 KB limit and the delivery id.
- Targets netstandard2.0 and net8.0, strong-named, with XML docs.