4. Examples

For the actual code and instructions on how to execute them, please check the Java samples in our dedicated Samples repository.

4.1. Sample Overview

Sample

Description

Key concept illustrated

HelloWorld

Submit a single task with inline input

Basic task submission

SharedBlob

Reuse one blob across multiple tasks

Blob reuse

TaskDependencies

Chain tasks using outputs as inputs

Task graph via blobs

DynamicLibrary

Load a library JAR at runtime

Dynamic worker loading

4.2. Task Graphs via Blobs

The blob abstraction is the mechanism that enables directed acyclic graphs (DAGs) of tasks in ArmoniK. The key insight is that an OutputBlobDefinition does not hold data — it is a named placeholder for a result that does not exist yet. When that placeholder is given to a subsequent task as an input, ArmoniK records the dependency and will not schedule the consuming task until the producing task has written to that blob.

4.2.1. How it works

  1. Submit task A, declaring an output blob result_a.

  2. Use the BlobHandle for result_a as the input of task B.

  3. Submit task B — it will only run once task A has completed and written result_a.

Task A ──writes──► blob result_a ──read by──► Task B

The control plane enforces this ordering automatically. No polling or manual synchronisation is needed on the client side.

4.2.2. TaskDependencies example

The TaskDependencies sample illustrates a simple fan-in graph:

task1 (1+2=3) ──┐
                ├──► task3 (3+7=10)
task2 (3+4=7) ──┘

The client submits all three tasks upfront. Tasks 1 and 2 are independent; task 3 declares the outputs of tasks 1 and 2 as its inputs, so the control plane holds task 3 until both predecessors complete.

On the client side, this is expressed by passing the BlobHandle of a previous task’s output directly into the next TaskDefinition:

// Task 1 — independent
var taskDef1 = new TaskDefinition()
    .withInput("num1", InputBlobDefinition.from("1".getBytes(UTF_8)))
    .withInput("num2", InputBlobDefinition.from("2".getBytes(UTF_8)))
    .withOutput("result");
var task1 = session.submitTask(taskDef1);

// Task 2 — independent
var taskDef2 = new TaskDefinition()
    .withInput("num1", InputBlobDefinition.from("3".getBytes(UTF_8)))
    .withInput("num2", InputBlobDefinition.from("4".getBytes(UTF_8)))
    .withOutput("result");
var task2 = session.submitTask(taskDef2);

// Task 3 — depends on outputs of task 1 and task 2
var taskDef3 = new TaskDefinition()
    .withInput("num1", task1.getOutput("result"))   // BlobHandle from task 1
    .withInput("num2", task2.getOutput("result"))   // BlobHandle from task 2
    .withOutput("result");
session.submitTask(taskDef3);

task1.getOutput("result") returns a BlobHandle — a reference to an output blob that has been declared but not yet written. Passing it as input to task 3 is what expresses the data dependency to the control plane.

4.2.3. SharedBlob example

The SharedBlob sample shows a complementary pattern: a single blob uploaded once and reused as input across multiple tasks. This avoids re-uploading the same data and lets several tasks read the same value concurrently.

var sharedBlob = session.createBlob(InputBlobDefinition.from("2".getBytes(UTF_8)));

session.submitTask(new TaskDefinition()
    .withInput("num1", sharedBlob)
    .withInput("num2", InputBlobDefinition.from("2".getBytes(UTF_8)))
    .withOutput("result"));

session.submitTask(new TaskDefinition()
    .withInput("num1", sharedBlob)
    .withInput("num2", InputBlobDefinition.from("3".getBytes(UTF_8)))
    .withOutput("result"));

4.2.4. Dynamic graphs from workers

Task graphs are not limited to what the client defines upfront. A worker can itself call context.submitTask(...), passing one of its own outputs as the input of a new child task. This enables workflows whose shape is determined at runtime — for example, a recursive divide-and-conquer where each task spawns sub-tasks only after inspecting its input.

See Worker — Subtask submission for the worker-side API.

4.3. Result Retrieval

Output blobs are surfaced through the BlobCompletionListener registered on the session. When any output blob transitions to COMPLETED or ABORTED, the listener is called asynchronously:

public class SimpleBlobListener implements BlobCompletionListener {
    @Override
    public void onBlobSuccess(Blob blob) {
        System.out.println("Data: " + new String(blob.data(), UTF_8));
    }

    @Override
    public void onBlobError(BlobError error) {
        System.out.println("Error: " + error.cause().getMessage());
    }
}

In a graph workflow, each leaf output blob fires a onBlobSuccess callback independently as soon as the task that produces it finishes, even if other branches of the graph are still running.