This is the abridged developer documentation for App-Compose # Intro You’ve covered the basics — quick start and installation. Now it’s time to go deeper. This section contains practical guides for common patterns and real-world scenarios you’ll encounter when building with App-Compose. * [x] getting task status * [x] map * [x] optional * [x] passing data to a task * [x] guard * [x] control task * [x] scope.get * [ ] graph * [ ] handling errors + observability = logging * [ ] tags creation (how not create too much) * [ ] usage with react (slots) # Getting a Task Result > App-Compose — Getting a Task Result Most of the time, you get Task results from Context. Sometimes you need the result after the app has finished running. For example, you might render a JSX that a Task returns once all stages complete. In that case, use the *Scope* returned by `compose().run()` and call `get()`. If a Task was skipped or failed, `get()` returns *undefined*. ```ts import { createRoot } from "react-dom/client" const alpha = createTask({ name: "alpha", run: { fn: () => ({ Greeting: () =>

Hello

, }), }, }) ;(async () => { // Wait for the app to finish running const scope = await compose().stage([alpha]).run() // Read the task result from the scope const alphaResult = scope.get(alpha) // If the task failed or was skipped, the result is undefined const Greeting = alphaResult?.Greeting if (Greeting) { createRoot(document.getElementById("root")).render() } })() ``` # Handling Errors > App-Compose — Handling Errors In real apps, things go wrong. A network request might fail, a data transformation might error out, or an external service might be unavailable. When this happens, App-Compose isolates the failure so the rest of your app continues to work as expected. Task errors only come from `run.fn` or `enabled.fn`. ## Failure behavior [Section titled “Failure behavior”](#failure-behavior) If a Task fails, App-Compose marks it as *failed* and keeps the app running. Any Task that strictly depends on it, directly or through Tags, is marked as *skipped*. ```ts import { status } from "@grlt-hub/app-compose" const alpha = createTask({ name: "alpha", run: { fn: () => { throw new Error("Oops!") }, }, }) const beta = createTask({ name: "alpha", run: { fn: () => {}, context: { alpha }, }, }) const gamma = createTask({ name: "gamma", run: { fn: (ctx) => { ctx.forEach((x) => console.log(x)) }, context: [ { "alpha.status.name": status(alpha).name, }, { "beta.status.name": status(beta).name }, ], }, }) compose({}).stage([alpha]).stage([beta]).stage([gamma]).run() ``` ## Logging Failures [Section titled “Logging Failures”](#logging-failures) By default, App-Compose logs a warning to the console when a Task fails. You can customize this behavior with the `onTaskFail`. This is useful for separating dev and production handling, for example sending errors to a logging system like Sentry. ```ts const alpha = createTask({ name: "alpha", run: { fn: () => { throw new Error("Oops!") }, }, }) const onTaskFail = (task) => { console.log(`*** ERROR *** \n ${String(task.id)} :: ${task.error}`) } compose({ // 👇 Handle task failures here log: { onTaskFail }, }) .stage([alpha]) .run() ``` # Inspecting App Topology > App-Compose — Inspecting App Topology As your app grows, understanding how different tasks interact can become difficult. Instead of digging through folders to trace dependencies, you can use the `.graph()` fn to get a clear map of your system. ```ts import { bind, compose, createTag, createTask, optional } from "@grlt-hub/app-compose" const tag = createTag({ name: "title" }) const alpha = createTask({ name: "alpha", run: { fn: () => ({ list: [0], title: "hello" }) }, }) const beta = createTask({ name: "beta", run: { context: { title: tag.value }, fn: console.log, }, }) const gamma = createTask({ name: "gamma", run: { context: { list: optional(alpha.result.list) }, fn: console.log, }, }) const graph = compose() .stage({ steps: [alpha] }) .stage({ steps: [bind(tag, alpha.result.title)] }) .stage({ steps: [beta, gamma] }) // 👇 use .graph() instead of .run() to get the dependency graph .graph() console.log(JSON.stringify(graph, null, 2)) ``` The `.graph()` returns a structured object representing your app topology. You can visualize this data using tools like Mermaid or render it as a simple text map to see the relationships between Tasks and Tags at a glance: ```plaintext [alpha] ──> ──> [beta] └ ─ ─ > [gamma] (optional) ``` # Control Task > App-Compose — Control Task Not all features are equally important. An analytics tracker failing is acceptable — a missing user session is not. When a critical feature fails, the app shouldn’t silently show a broken page; it should acknowledge the problem. A *control task* is a pattern for this. It observes the statuses of critical tasks and returns a result — `{ passed: true }` or `{ passed: false }` — that other parts of your app can use to decide what to do. Using `task.status` in context means the control task always runs — even when upstream tasks have failed or were skipped — which is what makes it a reliable observer. ```ts const control = createTask({ name: "control", run: { context: [fetchUser.status], fn: (ctx) => { const passed = ctx.every((status) => status === "done") return { ok: { passed } } }, }, }) compose() .stage({ steps: [fetchUser, analytics] }) .stage({ steps: [control] }) .run() .then((scope) => scope.get(control.result)) .then(console.log) ``` Try uncommenting the failure in `fetchUser` to see the control task catch it: ```ts import { compose, createTask } from "@grlt-hub/app-compose" // Critical — the page can't work without this const fetchUser = createTask({ name: "fetchUser", run: { fn: () => { // 👇 Uncomment to simulate failure // throw new Error("[fetchUser]: failed") return { ok: { id: 1, name: "Alice" } } }, }, }) // Non-critical — nice to have, but the page works without it const analytics = createTask({ name: "analytics", run: { fn: () => { throw new Error("service unavailable") }, }, }) // Control task: // observes critical tasks and decides if the app start succeeded const control = createTask({ name: "control", run: { context: [fetchUser.status], fn: (ctx) => { const passed = ctx.every((status) => status === "done") return { ok: { passed } } }, }, }) compose() .stage({ steps: [fetchUser, analytics] }) .stage({ steps: [control] }) .run() .then((scope) => scope.get(control.result)) .then(console.log) ``` # Checking Configuration > App-Compose — Checking Configuration *Guard* statically validates your composition before the app starts. It catches structural problems — like a task consuming context that was never provided — so they never reach production. Guard performs three checks. When an error is found, it stops immediately — fix it, then run again to catch the next one. | Check | Severity in `.run()` | Severity in `.guard()` | What it catches | | --------------- | -------------------- | ---------------------- | ------------------------------------------------------------------- | | **Duplicate** | error | error | A Task or Binding registered more than once across all stages | | **Unsatisfied** | error | error | A Task or Binding needs context not provided by any preceding stage | | **Unused** | warning | error | A Binding is never consumed by any Task or Binding | ## Built into run [Section titled “Built into run”](#built-into-run) Guard runs automatically every time you call `.run()`. If an error is found, `.run()` rejects before any task starts — the app won’t start with a broken configuration. ```ts compose() .stage({ steps: [bind(apiUrl, literal("https://api.example.com"))] }) .stage({ steps: [auth, dashboard] }) .run() // guard runs here, statically, before the app starts ``` Duplicate and unsatisfied issues reject with an error. Unused bindings print a `console.warn` — they don’t block the app but signal dead configuration worth cleaning up. ## Guard in CI [Section titled “Guard in CI”](#guard-in-ci) `.guard()` is the standalone version. It performs the same three checks but is stricter: unused bindings also throw, not just warn. Call it in a test to assert that your composition is correctly wired: ```ts import { describe, expect, it } from "vitest" import { bind, compose, literal } from "@grlt-hub/app-compose" import { apiUrl, auth, dashboard } from "./features" const stages = compose() .stage({ steps: [bind(apiUrl, literal("https://api.example.com"))] }) .stage({ steps: [auth] }) .stage({ steps: [dashboard] }) describe("app configuration", () => { it("is valid", () => { expect(() => stages.guard()).not.toThrow() }) }) ``` This makes broken wiring a CI failure. If a team member removes a task or renames a tag without updating the composition, the test catches it before the change ships. Try commenting out the first stage or uncommenting the duplicate to see guard catch it: ```ts const apiUrl = createTag({ name: "apiUrl" }) const auth = createTask({ name: "auth", run: { context: { url: apiUrl.value }, fn: ({ url }) => ({ token: "secret", url }), }, }) const dashboard = createTask({ name: "dashboard", run: { context: { token: auth.result.token }, fn: ({ token }) => console.log(`dashboard: token=${token}`), }, }) const stages = compose() // 👇 try commenting this out .stage({ steps: [bind(apiUrl, literal("https://api.example.com"))] }) .stage({ steps: [auth] }) // 👇 or uncomment this to add a duplicate // .stage({ steps: [auth] }) .stage({ steps: [dashboard] }) describe("app configuration", () => { it("is valid", () => { expect(() => stages.guard()).not.toThrow() }) }) ``` # Mapping Values > App-Compose — Mapping Values When you need only part of a task result or want to change its shape, `map` lets you transform a value before passing it as context to `run`, `enabled`, or a tag binding. Caution If the source value is missing — because an upstream task failed or was skipped — `map` will not call your callback and the downstream task will be silently skipped. To handle a missing value explicitly, wrap the source with `optional` — in that case the callback receives `undefined` instead. ```ts import { bind, compose, createTag, createTask, map } from "@grlt-hub/app-compose" const featureToggle = createTask({ name: "featureToggle", run: { fn: () => ({ dashboard: true }) }, }) const user = createTask({ name: "user", run: { fn: () => ({ id: 1, name: "Bob" }) }, }) const dashboardConfig = createTag<string>({ name: "dashboardConfig" }) const dashboard = createTask({ name: "dashboard", run: { // 👇 map in run context: map(dashboardConfig.value, (x) => x.toUpperCase()), fn: (ctx) => console.log(`dashboard for: ${ctx}`), }, enabled: { // 👇 map in enabled context: map(featureToggle.result, (x) => x.dashboard), fn: (enabled) => enabled, }, }) compose() .stage({ steps: [featureToggle, user] }) .stage({ steps: [ bind( dashboardConfig, // 👇 map in tag binding map(user.result, (x) => x.name), ), ], }) .stage({ steps: [dashboard], }) .run() ``` # Optional Values > App-Compose — Optional Values By default, every field in the context shape is **required**. If an upstream task failed or was skipped, any task that depends on its value is skipped as well — this is the safe default. But sometimes a task should run regardless of whether a particular value is available. A common example is an analytics tracker: it might be disabled in development or excluded from certain builds, yet the tasks that use it should still run normally. This is what `optional` is for. ```ts import { optional } from "@grlt-hub/app-compose" const checkout = createTask({ name: "checkout", run: { context: { analytics: optional(analytics.result), // analytics.result | undefined user: user.result, }, fn: (ctx) => { ctx.analytics?.track("checkout_started") console.log(`checkout for ${ctx.user.name}`) }, }, }) ``` Wrapping a field with `optional` changes its type from `T` to `T | undefined`. If the value isn’t available — because the upstream task failed, was skipped, or wasn’t included in the pipeline at all — the task still runs and receives `undefined`. Check out this interactive example. Try removing `analytics` from the first stage to see `checkout` still run: ```ts import { compose, createTask, optional } from "@grlt-hub/app-compose" const user = createTask({ name: "user", run: { fn: () => ({ name: "Bob" }) }, }) const analytics = createTask({ name: "analytics", run: { fn: () => ({ track: (event: string) => { console.log(`[analytics] track: ${event}`) }, }), }, }) const checkout = createTask({ name: "checkout", run: { context: { // 👇 analytics may or may not be in the pipeline analytics: optional(analytics.result), user: user.result, }, fn: (ctx) => { ctx.analytics?.track("checkout_started") console.log(`checkout for ${ctx.user.name}`) }, }, }) compose() // 👇 Try removing `analytics` — checkout still runs .stage({ steps: [analytics] }) .stage({ steps: [user] }) .stage({ steps: [checkout] }) .run() ``` Required fields in the shape still behave as before — if any of them is missing, the task is skipped. Only the fields explicitly wrapped with `optional` can be absent without affecting the task. ## With map [Section titled “With map”](#with-map) `optional` composes with `map`. When the source value is missing, `map`’s callback receives `undefined` — the task still runs, rather than being skipped: ```ts const checkout = createTask({ name: "checkout", run: { context: map(optional(analytics.result), (tracker) => tracker?.isEnabled ?? false), fn: (isTracked) => { console.log(`checkout: tracking=${isTracked}`) }, }, }) ``` ## In tag bindings [Section titled “In tag bindings”](#in-tag-bindings) When a tag is bound to an optional value, its type must allow `undefined` — otherwise it won’t match the tasks that use it. `optional` works in `bind` for exactly this case: if the source task is absent, the tag carries `undefined`: ```ts const analyticsTag = createTag<Analytics | undefined>({ name: "analytics" }) compose() .stage({ steps: [analytics] }) .stage({ steps: [bind(analyticsTag, optional(analytics.result))], }) .stage({ steps: [checkout] }) .run() ``` # Passing Data — Advanced > App-Compose — Passing Data to a Task In [Quick Start](/learn/quick-start/), you learned the basics: tasks receive data through a context, and tags keep them decoupled. This guide goes deeper — covering the two ways to provide data to a task and when to choose each. ## Via a Tag [Section titled “Via a Tag”](#via-a-tag) Using a tag is the recommended default. A task declares what it needs through a tag, and the pipeline decides what fills it. The task has no knowledge of where the data comes from — it only knows the contract. ```ts const userIdTag = createTag<number>({ name: "userId" }) const fetchUser = createTask({ name: "fetch-user", run: { context: { userId: userIdTag.value }, fn: ({ userId }) => { ... }, }, }) ``` The pipeline fills the tag using `bind`. The source can be a task result or a static value via `literal` — either way, `fetchUser` is not affected: ```ts // From a task result .stage({ steps: [bind(userIdTag, auth.result.userId)] }) // Or a static value — same tag, same task, different source .stage({ steps: [bind(userIdTag, literal(42))] }) ``` ```ts import { bind, compose, createTag, createTask } from "@grlt-hub/app-compose" const userIdTag = createTag<number>({ name: "userId" }) const auth = createTask({ name: "auth", run: { fn: () => { // 👇 Simulates a login result return { userId: 1 } }, }, }) const fetchUser = createTask({ name: "fetch-user", run: { // 👇 fetchUser only knows about the tag — not about auth context: { userId: userIdTag.value }, fn: async ({ userId }) => { const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`) const result = await response.json() console.log(JSON.stringify(result, null, 2)) }, }, }) compose() .stage({ steps: [auth] }) .stage({ steps: [bind(userIdTag, auth.result.userId)] }) // 👈 tag is filled here .stage({ steps: [fetchUser] }) .run() ``` ## Directly [Section titled “Directly”](#directly) You can also provide data directly in the context — either a static value or another task’s result: ```ts // static value baked into the task context: { userId: literal(1) } // another task's result referenced directly context: { userId: auth.result.userId } ``` Both couple the task to something specific. The task can no longer be reused in a different context without modifying it. Treat this as a deliberate choice, not a default. When in doubt, prefer a tag. ```ts import { compose, createTask, literal } from "@grlt-hub/app-compose" const auth = createTask({ name: "auth", run: { fn: () => ({ userId: 1 }), }, }) const fetchUser = createTask({ name: "fetch-user", run: { // 👇 directly coupled to auth — fetchUser can't exist without it context: { userId: auth.result.userId, url: literal("https://jsonplaceholder.typicode.com/users/"), }, fn: async (ctx) => { const response = await fetch(`${ctx.url}/${ctx.userId}`) const result = await response.json() console.log(JSON.stringify(result, null, 2)) }, }, }) compose() .stage({ steps: [auth] }) .stage({ steps: [fetchUser] }) .run() ``` # Using Task Status > App-Compose — Using Task Status Sometimes you need to know Task statuses. For example, you might want to show a fallback UI if core functionality fails. Each Task exposes a `.status` property that returns its current status: `done`, `skip`, or `fail`. ```js import { status } from "@grlt-hub/app-compose" const controlTask = createTask({ name: "control", run: { // Pass task statuses via context context: [fetchUser.status], fn: (ctx) => { /* Logic to handle checks */ }, }, }) ``` Caution `.status` is only available in `run.context`, `enabled.context`, or Tag bindings. You cannot use it inside arbitrary code like `run.fn`. Check out this interactive example to see `task.status` in action: ```ts import { compose, createTask } from "@grlt-hub/app-compose" const fetchUser = createTask({ name: "fetch-user", run: { fn: () => { // 👇 Uncomment to simulate failure // throw new Error("[fetch-user]: failed") }, }, }) const controlTask = createTask({ name: "control", run: { // 👇 Pass task statuses via context context: [fetchUser.status], fn: (ctx) => { const failure = ctx.some((status) => status === "fail") if (failure) { console.log("Something went wrong. Please try again.") } else { console.log("Everything is working!") } }, }, }) compose() .stage({ steps: [fetchUser] }) .stage({ steps: [controlTask] }) .run() ``` # Installation > App-Compose — Installation App-Compose is a lightweight and standalone library with zero dependencies. TypeScript support works out of the box. No separate `@types` packages are required. ## Try App-Compose [Section titled “Try App-Compose”](#try-app-compose) You don’t need to install anything to play with App-Compose. Try editing the [sandbox](/sandbox/) in a new tab. ## Install the package [Section titled “Install the package”](#install-the-package) Use your preferred package manager. * npm ```bash npm install --save-exact @grlt-hub/app-compose ``` * pnpm ```bash pnpm add --save-exact @grlt-hub/app-compose ``` * yarn ```bash yarn add --exact @grlt-hub/app-compose ``` * bun ```bash bun add --exact @grlt-hub/app-compose ``` ## Using with AI assistants [Section titled “Using with AI assistants”](#using-with-ai-assistants) ### Cursor [Section titled “Cursor”](#cursor) 1. Open chat and type `@docs` 2. Click **Add new doc** 3. Paste the URL and confirm: ```plaintext https://app-compose.dev/llms-full.txt ``` ### Claude / ChatGPT / Copilot [Section titled “Claude / ChatGPT / Copilot”](#claude--chatgpt--copilot) Paste this URL into the chat — most assistants accept a URL as context: ```plaintext https://app-compose.dev/llms-full.txt ``` Use [`llms-small.txt`](https://app-compose.dev/llms-small.txt) for models with a smaller context window (e.g. GPT-3.5, free-tier plans). # Linting > App-Compose — Linting We recommend using ESLint to enforce best practices and coding standards in your App-Compose projects.\ Please note that this plugin requires ESLint 9+ and TypeScript. ## Installation [Section titled “Installation”](#installation) Use your preferred package manager. * npm ```bash npm install --save-dev --save-exact @grlt-hub/eslint-plugin-app-compose ``` * pnpm ```bash pnpm add --save-dev --save-exact @grlt-hub/eslint-plugin-app-compose ``` * yarn ```bash yarn add --dev --exact @grlt-hub/eslint-plugin-app-compose ``` * bun ```bash bun add --dev --exact @grlt-hub/eslint-plugin-app-compose ``` ## Usage [Section titled “Usage”](#usage) Import the plugin and add it to your config. Use the recommended preset for a quick start. ```js import appCompose from "@grlt-hub/eslint-plugin-app-compose" export default tseslint.config(appCompose.configs.recommended) ``` You can also configure it manually. ```js { plugins: { 'app-compose': appCompose, }, rules: { 'app-compose/task-options-order': 'warn', }, } ``` ## Rules [Section titled “Rules”](#rules) ### task-options-order [Section titled “task-options-order”](#task-options-order) `createTask` accepts configuration in an object form. To maintain consistency and readability, configuration properties must follow the semantic order: `name` → `run.context` → `run.fn` → `enabled.context` → `enabled.fn`. This rule strictly enforces this sequence. ```ts // 👍 great createTask({ name: "alpha", run: { context: { timeout: timeoutTag }, fn: init }, enabled: { context: { authorized: authorizedTag }, fn: check }, }) // 👎 weird createTask({ name: "alpha", enabled: { fn: check, context: { authorized: authorizedTag } }, run: { context: { timeout: timeoutTag }, fn: init }, }) ``` # Quick Start > App-Compose — 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”](#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. ```js 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”](#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. ```ts 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. ```plaintext Stage 1 Stage 2 Stage 3 ┌──────────┐ ┌──────────┐ ┌──────┐ │ A B C │ → │ D E │ → │ F │ └──────────┘ └──────────┘ └──────┘ parallel parallel parallel ``` ## How to pass data to a Task [Section titled “How to pass data to a Task”](#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. ```ts 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. ```js import { bind } from "@grlt-hub/app-compose" .stage({ steps: [bind(tag, value)] }) .stage({ steps: [fetchUser] }) ``` Caution Do not mix `bind` and Tasks within the same Stage. ```js // ❌ Wrong .stage({ steps: [bind(...), task] }) // ✅ Correct .stage({ steps: [bind(...)] }).stage({ steps: [task] }) .stage({ steps: [bind(...)] }, { steps: [task] }) ``` Have a look at the result: ```ts 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`. ```ts 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() ``` Caution Prefer routing static values through a tag via `bind(tag, literal(value))`. Using `literal` directly in `run.context` bakes the value into the task — it can no longer be reused without modifying it. ## How to disable a Task [Section titled “How to disable a Task”](#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. ```js 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.context` is passed to `enabled.fn` * `enabled.fn` must return a *boolean | Promise\<boolean>* * If it returns false, the Task is skipped ```ts 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”](#next-steps) By now, you know how to build with App-Compose. Next, head over to [Installation](/learn/installation/) to get everything set up. # Using TypeScript > App-Compose — Using TypeScript App-Compose comes with built-in TypeScript support, so you can start writing type-safe code right away. ## TypeScript with Tags [Section titled “TypeScript with Tags”](#typescript-with-tags) When creating a Tag, you can specify the data type it holds. ```ts import { createTag } from "@grlt-hub/app-compose" const tag = createTag<string>({ name: "title" }) ``` This provides full type safety for both Tag inputs and outputs. ## Useful Types [Section titled “Useful Types”](#useful-types) ### TaskResult\<T> [Section titled “TaskResult\<T>”](#taskresultt) Extracts the return type of a given Task. ```ts import { type TaskResult, createTask } from "@grlt-hub/app-compose" const alpha = createTask({ name: "alpha", run: { fn: () => ({ value: "hello" }) }, }) type AlphaResult = TaskResult<typeof alpha> // => { value: string } ``` ### TaskStatus [Section titled “TaskStatus”](#taskstatus) Represents the possible execution outcomes of a Task. ```ts type TaskStatus = "done" | "fail" | "skip" ``` ### Task\<T> [Section titled “Task\<T>”](#taskt) Represents a Task created by `createTask`. Use it to type function parameters or variables that hold a Task reference. ```ts import { type Task, createTask } from "@grlt-hub/app-compose" const alpha = createTask({ name: "alpha", run: { fn: () => ({ value: "hello" }) }, }) // Use Task<T> to type a variable or function parameter that holds a Task reference const ref: Task<{ value: string }> = alpha ``` ### ComposeLogger [Section titled “ComposeLogger”](#composelogger) Defines lifecycle hooks for observing stage and task execution. ```ts import { type ComposeLogger } from "@grlt-hub/app-compose" const myLogger: ComposeLogger = { onTaskFail: (event) => console.log(event), onStageStart: (event) => console.log(event), onStageComplete: (event) => console.log(event), } ``` ### StageConfig [Section titled “StageConfig”](#stageconfig) Represents the configuration for a single stage passed to `compose`. ```ts import { type StageConfig, createTask } from "@grlt-hub/app-compose" const task = createTask({ name: "task", run: { fn: () => true } }) const stages: StageConfig = { steps: [task], } ``` # Example Reference > A reference page in my new Starlight docs site. Reference pages are ideal for outlining how things work in terse and clear terms. Less concerned with telling a story or addressing a specific use case, they should give a comprehensive outline of what you’re documenting. ## Further reading [Section titled “Further reading”](#further-reading) * Read [about reference](https://diataxis.fr/reference/) in the Diátaxis framework # Sandbox ```ts import { bind, compose, createTag, createTask, literal, optional } from "@grlt-hub/app-compose" const task = createTask({ name: "task", run: { fn: () => { console.log("Hello, World!") }, }, }) compose() .stage({ steps: [task] }) .run() ```