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 fromWorkflowException
| 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 fromio.serverlessworkflow.fluent.func.dsl.FuncDSL). -
The lower-level spec DSL:
http()and auth helpers (basic(…),bearer(…),digest(…),oidc(…),oauth2(…)) fromio.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 nameddemointo 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 fromapplication.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:
-
A Quarkus endpoint secured with HTTP Basic (
/secure/profile). -
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 byoperationId. -
.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 example401), and -
body is the
WorkflowErrorserialized 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
CompletionStagefrom your JAX-RS endpoint and callinstance(…).start()without blocking, anyWorkflowExceptionwill propagate directly and be handled by Quarkus Flow’sExceptionMapper<WorkflowException>. The mapper turns it into an RFC 7807 /WorkflowErrorHTTP response. -
If you choose the blocking style (
.start().get(),.join(), …), Java wraps the underlying exception inExecutionException. In that case you should either: (a) unwrap and rethrow theWorkflowException, or (b) prefer theCompletionStagestyle 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
-
Configure the HTTP client — timeouts, TLS, proxies and connection pooling.
-
Enable tracing — richer workflow-level traces and MDC fields.
-
Java DSL cheatsheet — HTTP / OpenAPI task builders and helpers.
-
CNCF Workflow mapping and concepts — background on CallHTTP and error semantics.