---
title: "Your first .NET pipeline"
description: "Run an HTTP worker, submit a typed stage, inspect its result, and stop cleanly."
category: ".NET SDK"
---

Build a console application that starts an HTTP worker, submits a greeting pipeline, waits for its result, and shuts down. [Download the example project](/downloads/HelloPipelogiq.zip) or follow the steps below. The handler runs in your application; Pipelogiq stores and schedules the work.

## Prerequisites

You need a .NET 8 SDK, a running Pipelogiq **0.5.0** server with its migrations applied, and an application API key. Complete the [server setup](/docs/installation) first. Use the external worker API URL, normally `http://localhost:8081` for local development, rather than the dashboard URL.

This application only needs outbound access to the API. The server still needs its configured infrastructure. For direct broker delivery, see [HTTP and AMQP workers](/docs/net/workers).

## Install the packages

These docs describe SDK **0.5.0**. On September 22, 2026, that version was not available from NuGet.org: the existing `PipelogiqSDK` index ended at `0.4.0-preview.1`, and the six split package indexes were absent. Use a local or private feed containing all seven `0.5.0` packages. An older public package is not a substitute for this tutorial.

### Prepare a feed from the current source

The following commands check out the public commit verified for these docs and pack the SDK locally. Run them in a directory outside your application. They require the .NET 8 SDK selected by the repository's `global.json` and internet access for third-party dependencies.

```bash
git clone https://github.com/pipelogiq/pipelogiq-sdk-net.git
cd pipelogiq-sdk-net
git checkout --detach 79fdf7ea22f5c972c9460099c8b4e6c2f26c926e
dotnet pack PipelogiqSdk.sln --configuration Release --output ./artifacts/docs-feed
```

Use the resulting `pipelogiq-sdk-net/artifacts/docs-feed` directory as the feed path below. It contains all seven SDK `.nupkg` files. These commands build local artifacts; they do not publish packages or run a Pipelogiq server. The [release preparation guide](https://github.com/pipelogiq/pipelogiq-sdk-net/blob/main/RELEASE.md) documents the full maintainer verification workflow.

Alternatively, use the exact `0.5.0` package folder supplied by your deployment team.

### Create the consumer

Create the application:

```bash
dotnet new console --framework net8.0 --name HelloPipelogiq
cd HelloPipelogiq
```

Create `NuGet.Config` in this application directory. Replace `/absolute/path/to/pipelogiq-packages` with the package folder. On Windows, use the corresponding absolute Windows path.

```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="pipelogiq" value="/absolute/path/to/pipelogiq-packages" />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
  <packageSourceMapping>
    <packageSource key="pipelogiq">
      <package pattern="PipelogiqSDK*" />
    </packageSource>
    <packageSource key="nuget.org">
      <package pattern="*" />
    </packageSource>
  </packageSourceMapping>
</configuration>
```

The specific `PipelogiqSDK*` mapping sends SDK packages to your feed; other dependencies come from NuGet.org. Then install:

```bash
dotnet add package PipelogiqSDK.Http --version 0.5.0
dotnet add package Microsoft.Extensions.Hosting --version 8.0.1
```

`PipelogiqSDK.Http` includes Core transitively. It does not include `RabbitMQ.Client`.

## Write the application

Replace the entire `Program.cs` with the following. There are no additional handler or model files to create.

```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PipelogiqSDK.Abstractions;
using PipelogiqSDK.Api;
using PipelogiqSDK.Builders;
using PipelogiqSDK.Configuration;
using PipelogiqSDK.Contracts;
using PipelogiqSDK.Http.Api;
using PipelogiqSDK.Http.Runner;
using PipelogiqSDK.StageHelper;

var apiKey = Environment.GetEnvironmentVariable("PIPELOGIQ_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
    throw new InvalidOperationException("Set PIPELOGIQ_API_KEY first.");

var options = new PipelogiqRunnerOptions
{
    ApiKey = apiKey,
    ApiUrl = Environment.GetEnvironmentVariable("PIPELOGIQ_API_URL")
        ?? "http://localhost:8081",
    WorkerName = "hello-http-worker",
    DrainGracePeriod = TimeSpan.FromSeconds(30)
};

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddPipelogiqHttpWorker(options);
builder.Services.AddTransient<HelloHandler>();
using var host = builder.Build();

var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
using var stop = CancellationTokenSource.CreateLinkedTokenSource(
    lifetime.ApplicationStopping);
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(stop.Token);
deadline.CancelAfter(TimeSpan.FromSeconds(90));

var runner = host.Services.GetRequiredService<HttpPipelineRunner>();
runner.RegisterHandler("HelloHandler", typeof(HelloHandler));
await host.StartAsync(stop.Token);
var workerTask = runner.StartAsync(stop.Token);

try
{
    // A new logical run gets a new key. Retrying this builder uses the same key.
    using var pipeline = PipelineBuilder.Create("hello-dotnet", options)
        .WithIdempotencyKey($"hello-{Guid.NewGuid():N}")
        .AddLabel("example", "dotnet-quickstart")
        .WithAction("greet", "HelloHandler", new HelloInput("world"));

    var created = await pipeline.SendAsync(deadline.Token);
    Console.WriteLine($"Pipeline {created.Id} created.");
    using var client = new PipelogiqApiClient(options);

    while (true)
    {
        if (workerTask.IsCompleted)
        {
            await workerTask; // Surface a worker failure before waiting further.
            throw new InvalidOperationException("The worker stopped early.");
        }

        var current = await client.GetPipelineAsync(created.Id, deadline.Token);
        if (current.IsTerminal ?? PipelineStatuses.IsTerminal(current.Status))
        {
            Console.WriteLine($"Pipeline {current.Id}: {current.Status}");
            foreach (var stage in current.Stages ?? new List<StageDto>())
                Console.WriteLine($"{stage.Name}: {stage.Status} — {stage.Output}");

            if (!string.Equals(current.Status, PipelineStatuses.Completed,
                    StringComparison.OrdinalIgnoreCase))
                Environment.ExitCode = 1;
            break;
        }

        await Task.Delay(TimeSpan.FromMilliseconds(500), deadline.Token);
    }
}
catch (OperationCanceledException) when (deadline.IsCancellationRequested)
{
    Console.WriteLine("Stopped waiting. The pipeline may still exist on the server.");
    Environment.ExitCode = 1;
}
finally
{
    stop.Cancel();
    try { await workerTask; }
    catch (OperationCanceledException) when (stop.IsCancellationRequested) { }
    finally
    {
        using var shutdown = new CancellationTokenSource(TimeSpan.FromSeconds(40));
        await host.StopAsync(shutdown.Token);
    }
}

internal sealed record HelloInput(string Name);

internal sealed class HelloHandler : IStageHandler<HelloInput>
{
    public Task<IStageResult> ExecuteAsync(
        HelloInput input, IStageContext? context = null)
    {
        context.GetCancellationToken().ThrowIfCancellationRequested();
        var greeting = $"Hello {input.Name}";
        context.AddItem("greeting", greeting);
        context.LogInfo("Greeting prepared.");
        return Task.FromResult<IStageResult>(StageResult.Success(greeting));
    }
}
```

## Run and inspect the result

Set configuration in your terminal; keep the key out of source control:

```bash
export PIPELOGIQ_API_URL="http://localhost:8081"
export PIPELOGIQ_API_KEY="your-application-api-key"
dotnet run
```

PowerShell equivalents:

```powershell
$env:PIPELOGIQ_API_URL = "http://localhost:8081"
$env:PIPELOGIQ_API_KEY = "your-application-api-key"
dotnet run
```

In addition to runtime logs, expect output similar to:

```text
Pipeline 123 created.
Pipeline 123: Completed
greet: Completed — Hello world
```

The numeric ID varies. Open that pipeline in the dashboard to inspect the stage input, output, log, and `greeting` context value. Submission returns a pipeline ID before the work finishes; the status loop observes completion separately.

The program stops the worker after observing a terminal result. Ctrl+C uses the host's shutdown signal. A timeout only stops this local example from waiting; it does not call the pipeline cancellation API.

## If it does not complete

| Symptom | Check |
|---|---|
| `NU1101` or missing version | Your configured feed contains both HTTP and Core `0.5.0` packages and their internal dependencies. |
| Authentication failure | Use an application API key and the external API URL. |
| Pipeline stays unstarted | API and scheduler are running; `HelloHandler` is registered under exactly the submitted name. |
| Worker repeatedly bootstraps | Check runtime logs, server compatibility, API reachability, and queue provisioning on the server. |
| Pipeline fails | Inspect each stage's output, `LastErrorCode`, attempts, and logs. |

Next, add [workflow dependencies and retries](/docs/net/workflows), or separate this producer and worker into different processes using the [worker guide](/docs/net/workers).
