Call HTTP and OpenAPI services

Quarkus Flow builds on top of the CNCF Workflow Specification to give you first-class HTTP and OpenAPI tasks.

This guide shows how to:

  • call any HTTP endpoint from a workflow

  • call operations defined in an OpenAPI document

  • configure logging for troubleshooting

  • understand the HTTP error shape you get back (RFC 7807 / CNCF WorkflowError) and how it is mapped from WorkflowException

For HTTP client timeouts, TLS, proxies and connection pooling, see Configure the HTTP client.

Prerequisites

  • A Quarkus application with Quarkus Flow set up.

  • At least one workflow class where you want to add HTTP or OpenAPI calls.

  • Basic familiarity with the CNCF Workflow Java DSL and Quarkus configuration.

1. Call HTTP endpoints

HTTP calls are represented in the CNCF Workflow Spec as HTTP Call tasks. In Quarkus Flow you normally interact with them through:

  • Func DSL shortcuts: get(…​), post(…​), etc. (imported from io.serverlessworkflow.fluent.func.dsl.FuncDSL).

  • The lower-level spec DSL: http() and auth helpers (basic(…​), bearer(…​), digest(…​), oidc(…​), oauth2(…​)) from io.serverlessworkflow.fluent.spec.dsl.DSL.

Under the hood they all produce an HTTP call task executed by the same HTTP client.

1.1 Simple GET/POST with Func DSL

For the majority of use cases you can rely on the Func DSL shortcuts:

import static io.serverlessworkflow.fluent.func.FuncWorkflowBuilder.workflow;
import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.get;

Workflow wf = workflow("simple-http")
                .tasks(get("https://api.example.com/health"))
                .build();

There are overloads that take a java.net.URI if you prefer:

URI endpoint = URI.create("https://api.example.com/customers/123");

Workflow wf = workflow("customer-get")
                .tasks(get(endpoint))
                .build();

Use post(…​) similarly for POST calls.

1.2 Add HTTP Basic authentication

To add auth, you combine the Func DSL shortcuts with the spec DSL auth helpers:

package org.acme.example;

import static io.serverlessworkflow.fluent.func.FuncWorkflowBuilder.workflow;
import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.get;
import static io.serverlessworkflow.fluent.spec.dsl.DSL.basic;
import static io.serverlessworkflow.fluent.spec.dsl.DSL.secret;

import java.net.URI;

import jakarta.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.config.inject.ConfigProperty;

import io.quarkiverse.flow.Flow;
import io.serverlessworkflow.api.types.Workflow;

@ApplicationScoped
public class CustomerProfileFlow extends Flow {

    @ConfigProperty(name = "demo.server", defaultValue = "http://localhost:8080")
    String securedServer;

    @Override
    public Workflow descriptor() {
        URI endpoint = URI.create(securedServer + "/secure/profile");

        return workflow("secure-customer-profile")
                // Load secrets into workflow data (see secrets documentation)
                .use(secret("demo"))
                .tasks(
                        // GET with HTTP Basic credentials taken from the secret
                        get(endpoint, basic("${ $secret.demo.username }", "${ $secret.demo.password }")))
                .build();
    }
}

Key points:

  • use(secret("demo")) loads a secret bundle named demo into the workflow context.

  • ${ $secret.demo.username } and ${ $secret.demo.password } are workflow expressions that resolve to the values stored by the secret manager (for example from application.properties).

  • basic(…​) configures HTTP Basic auth on the outgoing request.

In your application.properties you typically have:

# Wire Quarkus HTTP Basic auth to users/roles files

quarkus.http.auth.basic=true
quarkus.security.users.file.enabled=true
quarkus.security.users.file.users=users.properties
quarkus.security.users.file.roles=roles.properties
quarkus.security.users.file.realm-name=quarkus-flow
quarkus.security.users.file.plain-text=true

# Protect /secure/*: must be authenticated

quarkus.http.auth.permission.secure.policy=authenticated
quarkus.http.auth.permission.secure.paths=/secure/*

# Demo service account credentials

demo.username=alice
demo.password=secret

This lets you showcase a full end-to-end story:

  1. A Quarkus endpoint secured with HTTP Basic (/secure/profile).

  2. A workflow that uses HTTP Call to call that endpoint using credentials stored as secrets.

1.3 Other authentication modes

The spec DSL also provides helpers for other auth schemes:

import static io.serverlessworkflow.fluent.spec.dsl.DSL.*;

// Bearer token
get("https://api.example.com/v1/resource", bearer("token-123"));

// Digest
get("https://api.example.com/v1/resource", digest("bob", "p@ssw0rd"));

// OIDC / OAuth2 (client credentials)
call(http().POST().endpoint("https://api.example.com/v1/resource",
    oidc("https://auth.example.com/", OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS,
        "client-id",
        "client-secret")));

// Alias for OIDC without explicit client
call(http().POST().endpoint("https://api.example.com/v1/resource",
    oauth2("https://auth.example.com", OAuth2AuthenticationData.OAuth2AuthenticationDataGrant.CLIENT_CREDENTIALS)));

These are useful when you need to call external identity providers or APIs protected by OAuth2 / OIDC.

1.4 Use the low-level HTTP builder

When you need HTTP methods other than GET/POST or want to fine-tune the endpoint, you can drop down to the HTTP builder from the spec DSL and wrap it in call(…​):

import static io.serverlessworkflow.fluent.spec.dsl.DSL.call;
import static io.serverlessworkflow.fluent.spec.dsl.DSL.http;

Workflow wf = WorkflowBuilder.workflow("update-customer", "ns", "1")
                .tasks(call(http().PUT().uri("https://api.example.com/customers/123", basic("u", "p"))))
                .build();

Use this style when you need full control over the HTTP Call arguments. For day-to-day Quarkus Flow development, the Func DSL shortcuts (get, post, etc.) are usually enough.

2. Call OpenAPI operations

OpenAPI tasks are a higher-level wrapper around CallHTTP that:

  • read an OpenAPI document (JSON or YAML)

  • let you call an operation by its operationId

  • take care of building the correct HTTP method, path and content type

In the Func DSL this is exposed via the openapi() builder.

2.1 Place the OpenAPI file

In Quarkus, the most convenient place for example/demo specs is under src/main/resources.

For example, in the Petstore demo:

src/
    main/
        java/...
        resources/
            openapi/
                petstore.json

You then point the OpenAPI task at it using a classpath URI:

final URI petstoreUri = URI.create("openapi/petstore.json");

You can also use an absolute URI to load specs from HTTP/file if needed.

2.2 Call operations by operationId

Each operation in an OpenAPI document has an operationId. For example, in the Petstore spec you will find:

{
  "paths": {
    "/pet/findByStatus": {
      "get": {
        "operationId": "findPetsByStatus"
      }
    }
  }
}

In your workflow, you reference the operation by that id:

package org.acme.example;

import static io.serverlessworkflow.fluent.func.FuncWorkflowBuilder.workflow;
import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.openapi;

import java.net.URI;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkiverse.flow.Flow;
import io.serverlessworkflow.api.types.Workflow;

@ApplicationScoped
public class PetstoreFlow extends Flow {

    @Override
    public Workflow descriptor() {
        final URI petstoreUri = URI.create("openapi/petstore.json");

        return workflow("petstore")
                .tasks(
                        // 1) Find pets by status
                        openapi()
                                .document(petstoreUri).operation("findPetsByStatus").parameter("status", "sold")
                                // Pick the first pet id and expose it as `selectedPetId`
                                .outputAs("${ { selectedPetId: .[0].id } }"),
                        // 2) Fetch that pet by id
                        openapi()
                                .document(petstoreUri).operation("getPetById").parameter("petId", "${ .selectedPetId }"))
                .build();
    }
}

Notes:

  • document(petstoreUri) tells the builder which OpenAPI document to read.

  • operation("findPetsByStatus") selects the operation by operationId.

  • .parameter("status", "sold") configures the path/query parameter. The actual mapping (path vs query vs body) is derived from the OpenAPI spec.

  • You can use workflow expressions (${ …​ }) for parameter values to dynamically pull from the workflow data.

3. Logging and troubleshooting

For HTTP and OpenAPI tasks you usually want to see both:

  • the workflow-level tracing (which task ran, what error occurred)

  • the HTTP client logs (method, URL, status, and optionally body)

A typical configuration:

# Enable Quarkus Flow tracing
quarkus.flow.tracing.enabled=true

# Log HTTP requests/responses performed by CallHTTP/OpenAPI tasks
quarkus.flow.http.client.logging.scope=request-response
quarkus.flow.http.client.logging.body-limit=2048
quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=DEBUG

When something goes wrong you will see:

  • a tracing line for the failing task (including the WorkflowError)

  • the HTTP request/response pair for the exact call that failed

This makes it much easier to debug connectivity, auth and payload issues.

For even richer tracing (including MDC with workflow instance id, task id, etc.) see Enable tracing.

4. HTTP errors and WorkflowException mapping

When an HTTP or OpenAPI task fails (4xx/5xx, timeouts, etc.), the workflow engine throws a WorkflowException containing a WorkflowError.

WorkflowError follows the CNCF Workflow Spec error model, which is aligned with RFC 7807 Problem Details:

{
  "type": "https://serverlessworkflow.io/spec/1.0.0/errors/communication",
  "status": 401,
  "instance": "do/0/370b9a91-22c2-4b3e-85c4-10892c2864ff",
  "title": "HTTP 401 Unauthorized",
  "details": null
}

Quarkus Flow provides an exception mapper that turns WorkflowException into an HTTP response whose:

  • status code is error.status (for example 401), and

  • body is the WorkflowError serialized as JSON (Problem Details style).

4.1 CompletionStage vs blocking style

How HTTP errors surface in your REST resources depends on how you call the workflow:

  • If you return a CompletionStage from your JAX-RS endpoint and call instance(…​).start() without blocking, any WorkflowException will propagate directly and be handled by Quarkus Flow’s ExceptionMapper<WorkflowException>. The mapper turns it into an RFC 7807 / WorkflowError HTTP response.

  • If you choose the blocking style (.start().get(), .join(), …), Java wraps the underlying exception in ExecutionException. In that case you should either: (a) unwrap and rethrow the WorkflowException, or (b) prefer the CompletionStage style so the mapper can see the original exception.

The mapper is not limited to HTTP/OpenAPI tasks: any JAX-RS resource that throws WorkflowException (directly, or via a non-blocked CompletionStage) will be handled by the same mapper and produce an RFC 7807 response.

A recommended pattern for REST endpoints is:

package org.acme.example;

import java.util.Map;
import java.util.concurrent.CompletionStage;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/api/profile")
@Produces(MediaType.APPLICATION_JSON)
public class CustomerProfileResource {

    @Inject
    CustomerProfileFlow customerProfileFlow;

    @GET
    public CompletionStage<Map<String, Object>> getProfileViaWorkflow() {
        return customerProfileFlow.instance(Map.of()).start().thenApply(r -> r.asMap().orElseThrow());
    }
}

If the HTTP/OpenAPI task succeeds, you get a 200 OK with the workflow data as JSON.

If the HTTP call fails (for example 401 Unauthorized), Quarkus Flow’s WorkflowException mapper will automatically return a response like:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "type": "https://serverlessworkflow.io/spec/1.0.0/errors/communication",
  "status": 401,
  "instance": "do/0/...",
  "title": "HTTP 401 Unauthorized",
  "details": null
}

This keeps HTTP and OpenAPI task errors predictable and machine-friendly while still exposing the full workflow error semantics defined by the CNCF Workflow Specification.

See also