Quick Start
App-Compose is built on two primitives: Task and Tag. Everything else — isolation, reuse, and predictable startup — flows from these core concepts. This guide teaches you how to use them.
How to create a Task
Section titled “How to create a Task”A Task is a piece of logic that has its own identity and execution function. A Task can be as small as a single API request, or as large as a product feature.
const fetchUser = createTask({ name: "fetch-user", run: { fn: async () => { const response = await fetch("https://jsonplaceholder.typicode.com/users/1") const result = await response.json()
console.log(JSON.stringify(result, null, 2)) return result }, },})Note that every Task must have a name and a run.fn. These are the only mandatory properties. The run.fn can be either a synchronous or an asynchronous function. It can return a value to be used by other Tasks, or return nothing if the Task only performs a side effect.
How to run a Task
Section titled “How to run a Task”Tasks cannot be called directly. To run a Task, first define it on a Stage. A Stage is a logical phase in your application’s workflow.
This gives you precise control over the execution flow, prioritizing critical Tasks before non-essential ones.
After defining the Stages, call run() to start execution.
import { compose, createTask } from "@grlt-hub/app-compose"
const fetchUser = createTask({ name: "fetch-user", run: { fn: async () => { const response = await fetch("https://jsonplaceholder.typicode.com/users/1") const result = await response.json()
console.log(JSON.stringify(result, null, 2)) return result }, },})
compose() .stage({ steps: [fetchUser] }) .run()Steps within the same Stage run in parallel. Stages execute one after another — the next Stage starts only when all Steps in the current one have finished.
Stage 1 Stage 2 Stage 3┌──────────┐ ┌──────────┐ ┌──────┐│ A B C │ → │ D E │ → │ F │└──────────┘ └──────────┘ └──────┘ parallel parallel parallelHow to pass data to a Task
Section titled “How to pass data to a Task”Tasks rarely operate in a vacuum. To be dynamic and reusable, they need access to external data like a username, a product ID, or the current interface language. Tasks access data through a Context, receiving everything they need from the environment without hardcoding values.
run.fn receives the resolved value of run.context.
To share data without linking tasks together, use Tag. A Tag is a placeholder for data that avoids direct imports between Tasks, keeping them isolated and reusable across any workflow.
import { createTag } from "@grlt-hub/app-compose"
const tag = createTag({ name: "userId" })The bind operator fills a Tag at the Stage level, making data available for upcoming Tasks.
import { bind } from "@grlt-hub/app-compose"
.stage({ steps: [bind(tag, value)] }).stage({ steps: [fetchUser] })Have a look at the result:
import { bind, compose, createTag, createTask } from "@grlt-hub/app-compose"
const tag = createTag<number>({ name: "userId" })
const fetchUser = createTask({ name: "fetch-user", run: { context: { userId: tag.value }, fn: async (ctx) => { const response = await fetch( // 👇 userId is passed from Context `https://jsonplaceholder.typicode.com/users/${ctx.userId}`, ) const result = await response.json()
console.log(JSON.stringify(result, null, 2)) }, },})
const logIn = createTask({ name: "log-in", run: { fn: () => { // 👇 Try different values here return { id: 1 } }, },})
compose() .stage({ steps: [logIn] }) .stage({ steps: [bind(tag, logIn.result.id)] }) .stage({ steps: [fetchUser] }) .run()Sometimes a value doesn’t come from another Task — it’s just a plain string, number, or object known upfront. To pass a static value, wrap it in literal.
This tells App-Compose that the value is data, not a reference to another Task or Tag. It also lets the library skip searching through complex structures like circular references or custom class instances — keeping your application fast.
literal works the same way everywhere: in context and in bind.
import { bind, compose, createTag, createTask, literal } from "@grlt-hub/app-compose"
const tag = createTag({ name: "userId" })
const fetchUser = createTask({ name: "fetch-user", run: { context: { // 👇 Static value for context baseUrl: literal("https://jsonplaceholder.typicode.com/users/"), userId: tag.value, }, fn: async (ctx) => { const response = await fetch(`${ctx.baseUrl}/${ctx.userId}`) const result = await response.json()
console.log(JSON.stringify(result, null, 2)) }, },})
compose() // 👇 Binding a Tag to a static value .stage({ steps: [bind(tag, literal(1))] }) .stage({ steps: [fetchUser] }) .run()How to disable a Task
Section titled “How to disable a Task”Tasks are enabled by default. Use the enabled property to skip a Task based on feature toggles, application state, or any internal logic.
const tag = createTag({ name: "fetch-user::enabled" })
const fetchUser = createTask({ name: "fetch-user", run: { fn: () => {...} }, enabled: { context: { enabled: tag.value }, fn: ({ enabled }) => enabled, },})The enabled property works as follows:
- Data from
enabled.contextis passed toenabled.fn enabled.fnmust return a boolean | Promise<boolean>- If it returns false, the Task is skipped
const featureFlags = createTask({ name: "feature-flags", run: { fn: () => { // 👇 true = Task runs, false = Task is skipped const flags = { fetchingAllowed: true } console.log(flags)
return flags }, },})
const tag = createTag({ name: "fetch-user::enabled" })
const fetchUser = createTask({ name: "fetch-user", run: { fn: () => console.log("[fetch-user]: user fetched"), }, enabled: { context: { enabled: tag.value }, // 👇 Return false to skip this Task fn: ({ enabled }) => enabled, },})
compose() .stage({ steps: [featureFlags] }) .stage({ steps: [bind(tag, featureFlags.result.fetchingAllowed)] }) .stage({ steps: [fetchUser] }) .run()Next Steps
Section titled “Next Steps”By now, you know how to build with App-Compose.
Next, head over to Installation to get everything set up.