<SYSTEM>This is the full developer documentation for App-Compose</SYSTEM>

# Overview

> Helper utilities for App-Compose.

App-Coda is App-Compose’s companion package. It wraps recurring Task and Wire patterns as named helpers, so you call one instead of rebuilding the same wiring.

Missing one or have an idea? [Tell us](/community/) or send a PR.

## 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-coda
  ```

* pnpm

  ```bash
  pnpm add --save-exact @grlt-hub/app-coda
  ```

* yarn

  ```bash
  yarn add --exact @grlt-hub/app-coda
  ```

* bun

  ```bash
  bun add --exact @grlt-hub/app-coda
  ```

## AI tools

[Section titled “AI tools”](#ai-tools)

App-Coda ships an LLM-friendly subset of the docs.

### 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-txt/app-coda.txt
```

### Claude / ChatGPT / Copilot

[Section titled “Claude / ChatGPT / Copilot”](#claude--chatgpt--copilot)

Paste this URL into the chat — most assistants accept URLs as context:

```plaintext
https://app-compose.dev/_llms-txt/app-coda.txt
```

# debug

> A runtime inspection Task — logs Tasks, Tags, and Spots to the console.

`debug` returns a Task that prints state of Tasks, Tags, and Spots to the console — grouped per target. Without it, you’d write that Task by hand and wrap every read in `optional`:

```diff
-createTask({
  -name: "debug",
  -run: {
    -context: {
      -auth: {
        -status: auth.status,
        -result: optional(auth.result),
        -error: optional(auth.error),
-      },
      -userId: optional(userId.value),
-    },
    -fn: (ctx) => {
      -console.group("[Task]: auth")
      -console.info(ctx.auth)
      -console.groupEnd()
      -console.group("[Tag]: userId")
      -console.info(ctx.userId)
      -console.groupEnd()
-    },
-  },
-})


+debug(auth, userId)
```

## Console output

[Section titled “Console output”](#console-output)

```text
▾ [app-coda] debug
  ▾ [Task]: auth
    { status: …, result: …, error: … }
  ▾ [Tag]: userId
    …
```

## Arguments

[Section titled “Arguments”](#arguments)

`targets` — Tasks, Tags, and Spots to inspect. A mix is fine.

## Returns

[Section titled “Returns”](#returns)

`Task` — a Task you compose with `.step(...)`.

## With options

[Section titled “With options”](#with-options)

Pass an options object first to set the console group label:

```diff
-debug(auth, fetchOrder)


+debug({ name: "post-login" }, auth, fetchOrder)
```

```text
▾ [app-coda] debug post-login
  …
```

### Arguments

[Section titled “Arguments”](#arguments-1)

* `options` — `{ name: string }` sets the group label in the console.
* `targets` — Tasks, Tags, and Spots to inspect. A mix is fine.

### Returns

[Section titled “Returns”](#returns-1)

`Task` — a Task you compose with `.step(...)`.

# every

> All-of quantifier for Tasks, Tags, and Spots — by predicate or by Task status.

`every` checks a whole list of Tasks, Tags, or Spots against one condition. Without it, you’d build the same `shape` by hand:

```ts
// without every
const allEven = shape([fetchUser.result, fetchOrder.result], (results) =>
  results.every((result) => result.id % 2 === 0),
)


// with every
const allEven = every([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0)
```

## Arguments

[Section titled “Arguments”](#arguments)

* `list` — Tasks, Tags, and Spots to check. A mix is fine.
* `predicate` — runs once per item. A Task is unwrapped to `{ result, status, error }`; a Tag or Spot, to its value. Must return a `boolean`.

## Returns

[Section titled “Returns”](#returns)

`Spot<boolean>` — `true` when every item satisfies `predicate`; `false` otherwise.

## every.status

[Section titled “every.status”](#everystatus)

`every.status` checks that every Task in a list reached the same status. Without it, you’d write that as the `every` predicate by hand:

```ts
// without every
const allDone = shape([fetchUser.status, fetchOrder.status], (statuses) =>
  statuses.every((status) => status === "done"),
)


// with every
const allDone = every([fetchUser, fetchOrder], (task) => task.status === "done")


// with every.status
const allDone = every.status([fetchUser, fetchOrder], "done")
```

### Arguments

[Section titled “Arguments”](#arguments-1)

* `list` — Tasks
* `status` — the `TaskStatus` every Task must reach.

### Returns

[Section titled “Returns”](#returns-1)

`Spot<boolean>` — `true` when every Task reached `status`; `false` otherwise.

# not

> Inverts a Spot into its boolean opposite.

`not` inverts a Spot into its boolean opposite. Without it, you’d build the same `shape` by hand:

```ts
// without not
const userMissing = shape(fetchUser.result, (user) => !user)
// with not
const userMissing = not(fetchUser.result)


// without not
const notAllDone = shape(every.status([fetchUser, fetchOrder], "done"), (allDone) => !allDone)
// with not
const notAllDone = not(every.status([fetchUser, fetchOrder], "done"))
```

## Arguments

[Section titled “Arguments”](#arguments)

* `value: Spot<T>` — a Spot, coerced to a boolean and inverted.

## Returns

[Section titled “Returns”](#returns)

`Spot<boolean>` — `true` when the input is falsy; `false` otherwise.

# some

> Any-of quantifier for Tasks, Tags, and Spots — by predicate or by Task status.

`some` checks a list of Tasks, Tags, or Spots for any item that matches one condition. Without it, you’d build the same `shape` by hand:

```ts
// without some
const anyEven = shape([fetchUser.result, fetchOrder.result], (results) =>
  results.some((result) => result.id % 2 === 0),
)


// with some
const anyEven = some([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0)
```

## Arguments

[Section titled “Arguments”](#arguments)

* `list` — Tasks, Tags, and Spots to check. A mix is fine.
* `predicate` — runs once per item. A Task is unwrapped to `{ result, status, error }`; a Tag or Spot, to its value. Must return a `boolean`.

## Returns

[Section titled “Returns”](#returns)

`Spot<boolean>` — `true` when any item satisfies `predicate`; `false` otherwise.

## some.status

[Section titled “some.status”](#somestatus)

`some.status` checks whether any Task in a list reached a given status. Without it, you’d write that as the `some` predicate by hand:

```ts
// without some
const anyDone = shape([fetchUser.status, fetchOrder.status], (statuses) =>
  statuses.some((status) => status === "done"),
)


// with some
const anyDone = some([fetchUser, fetchOrder], (task) => task.status === "done")


// with some.status
const anyDone = some.status([fetchUser, fetchOrder], "done")
```

### Arguments

[Section titled “Arguments”](#arguments-1)

* `list` — Tasks
* `status` — the `TaskStatus` at least one Task must reach.

### Returns

[Section titled “Returns”](#returns-1)

`Spot<boolean>` — `true` when any Task reached `status`; `false` otherwise.

# when

> Bundles `every`, `some`, and `not` into a `{ context, fn: Boolean }` accepted by task.enabled or task.run.

`when` bundles `every`, `some`, and `not` into `{ context: Spot<boolean>, fn: typeof Boolean }` — accepted by `task.enabled` and `task.run`.

## when.every

[Section titled “when.every”](#whenevery)

Same as `every`.

```diff
createTask({
  name: "_",
  run: { fn: console.log },
  -enabled: {
    -context: every([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0),
    -fn: Boolean,
-  },
})


createTask({
  name: "_",
  run: { fn: console.log },
  +enabled: when.every([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0),
})
```

## when.every.status

[Section titled “when.every.status”](#wheneverystatus)

Same as `every.status`.

```diff
createTask({
  name: "_",
  run: { fn: console.log },
  -enabled: {
    -context: every.status([fetchUser, fetchOrder], "done"),
    -fn: Boolean,
-  },
})


createTask({
  name: "_",
  run: { fn: console.log },
  +enabled: when.every.status([fetchUser, fetchOrder], "done"),
})
```

## when.some

[Section titled “when.some”](#whensome)

Same as `some`.

```diff
createTask({
  name: "_",
  run: { fn: console.log },
  -enabled: {
    -context: some([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0),
    -fn: Boolean,
-  },
})


createTask({
  name: "_",
  run: { fn: console.log },
  +enabled: when.some([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0),
})
```

## when.some.status

[Section titled “when.some.status”](#whensomestatus)

Same as `some.status`.

```diff
createTask({
  name: "_",
  run: { fn: console.log },
  -enabled: {
    -context: some.status([fetchUser, fetchOrder], "fail"),
    -fn: Boolean,
-  },
})


createTask({
  name: "_",
  run: { fn: console.log },
  +enabled: when.some.status([fetchUser, fetchOrder], "fail"),
})
```

## when.not

[Section titled “when.not”](#whennot)

Same as `not`.

```diff
createTask({
  name: "_",
  run: { fn: console.log },
  -enabled: {
    -context: not(fetchUser.result),
    -fn: Boolean,
-  },
})


createTask({
  name: "_",
  run: { fn: console.log },
  +enabled: when.not(fetchUser.result),
})
```

## when.not.every

[Section titled “when.not.every”](#whennotevery)

Inverts `every`.

```diff
createTask({
  name: "_",
  run: { fn: console.log },
  -enabled: {
    -context: not(every([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0)),
    -fn: Boolean,
-  },
})


createTask({
  name: "_",
  run: { fn: console.log },
  +enabled: when.not.every([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0),
})
```

## when.not.every.status

[Section titled “when.not.every.status”](#whennoteverystatus)

Inverts `every.status`.

```diff
createTask({
  name: "_",
  run: { fn: console.log },
  -enabled: {
    -context: not(every.status([fetchUser, fetchOrder], "done")),
    -fn: Boolean,
-  },
})


createTask({
  name: "_",
  run: { fn: console.log },
  +enabled: when.not.every.status([fetchUser, fetchOrder], "done"),
})
```

## when.not.some

[Section titled “when.not.some”](#whennotsome)

Inverts `some`.

```diff
createTask({
  name: "_",
  run: { fn: console.log },
  -enabled: {
    -context: not(some([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0)),
    -fn: Boolean,
-  },
})


createTask({
  name: "_",
  run: { fn: console.log },
  +enabled: when.not.some([fetchUser, fetchOrder], (task) => task.result.id % 2 === 0),
})
```

## when.not.some.status

[Section titled “when.not.some.status”](#whennotsomestatus)

Inverts `some.status`.

```diff
createTask({
  name: "_",
  run: { fn: console.log },
  -enabled: {
    -context: not(some.status([fetchUser, fetchOrder], "fail")),
    -fn: Boolean,
-  },
})


createTask({
  name: "_",
  run: { fn: console.log },
  +enabled: when.not.some.status([fetchUser, fetchOrder], "fail"),
})
```

# Community

> Channels for users and contributors of app-compose.

Three channels — pick the one that fits how you want to talk.

[**GitHub Discussions**searchableOpen-ended questions and how-tos. Start here when you’re not sure where to ask.](https://github.com/grlt-hub/app-compose/discussions)[**GitHub Issues**bugs & featuresReproducible bugs, concrete feature requests. Include a minimal repro.](https://github.com/grlt-hub/app-compose/issues/new)[**Telegram**real-timeLive chat. Quick replies, less structure than a thread.](https://t.me/app_compose)

# Overview

> Pattern guides for App-Compose — one common task per page, from light to deep.

Each guide covers one App-Compose pattern. Listed from light to deep — read in order or jump in.

Missing one? [Tell us](/community/) or send a PR.

# Ship only enabled features

> Ship a feature's code only when it's enabled — load it dynamically inside run.fn.

A Task runs only when its `enabled` returns true. Otherwise it’s skipped — `run.fn` never executes.

But a skipped Task still ships its code. Static imports go into the bundle whether the Task runs or not.

Load a feature’s code with a dynamic `import()` inside `run.fn`, and the bundler emits it as a separate chunk. The browser fetches it only when the Task runs.

Disable the Task, and the chunk never loads.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask } from "@grlt-hub/app-compose"
import { themeSelectFeature } from "./theme-select"


const themeSelect = createTask({
  name: "theme-select",
  run: { fn: () => themeSelectFeature() },
  // Task is disabled — but the static import above already loaded the module
  enabled: { fn: () => false },
})


const languageSelect = createTask({
  name: "language-select",
  run: {
    fn: async () => {
      const { languageSelectFeature } = await import("./language-select")
      languageSelectFeature()
    },
  },
  // Task is disabled — the dynamic import never runs, so the module never loads
  enabled: { fn: () => false },
  // 👇 uncomment to enable — the module will load
  // enabled: { fn: () => true },
})


compose().step([themeSelect, languageSelect]).run()
```

# Debugging your composition

> Inspect Tasks, Tags, and Spots inside a compose chain — runtime state in the console.

You don’t see what happens inside a compose chain — what each Task returned, which were skipped, what your Tags carried, or what every Spot computed.

When something goes wrong, you want to see inside — without scattering `console.log` across every Task. There are a few ways to do that.

## scope.get

[Section titled “scope.get”](#scopeget)

`compose(...).run()` returns `Promise<Scope>` — `await` it to get a `Scope` with one method, `scope.get(spot)`, that reads any **Spot** computed during the run. A Spot is any readable source in the composition: `task.result`, `task.status`, `task.error`, `tag.value`, anything from `shape(...)`. The call returns `undefined` if the source wasn’t computed (a skipped or failed Task).

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, createWire, tag } from "@grlt-hub/app-compose"


const userId = tag<number>("userId")


const fetchUser = createTask({
  name: "fetch-user",
  run: { fn: () => ({ id: 1 }) },
})


const loadCart = createTask({
  name: "load-cart",
  run: {
    context: userId.value,
    fn: () => {
      throw new Error("cart service is down")
    },
  },
})


;(async () => {
  const scope = await compose()
    .step(fetchUser)
    .step(createWire({ from: fetchUser.result.id, to: userId }))
    .step(loadCart)
    .run()


  console.log(`[fetchUser.status]: ${scope.get(fetchUser.status)}`)
  console.log(`[fetchUser.result]: ${JSON.stringify(scope.get(fetchUser.result))}`)
  console.log(`[userId]: ${scope.get(userId.value)}`)
  console.log(`[loadCart.status]: ${scope.get(loadCart.status)}`)
  console.log(`[loadCart.error]: ${scope.get(loadCart.error)}`)
})()
```

**Good for**

* One place — all your reads cluster after the run.

**Trade-offs**

* Manual — you pick what to read and log, by hand.
* End-only — you see the final state of each Spot, not snapshots from mid-run.

## Debug Task

[Section titled “Debug Task”](#debug-task)

Add a Task that reads any Tasks, Tags, or Spots via `context` and logs them. Place it between `.step(...)` calls to see state at that point in the chain.

The catch: every `context` read must be wrapped in [`optional`](/reference/optional) — `optional(task.result)`, `optional(tag.value)`, and so on. Without it, the debug Task is skipped along with any source that fails or is skipped — and you get no log.

[debug](/app-coda/debug) from `@grlt-hub/app-coda` handles this wrapping for you. Pass any Tasks, Tags, or Spots; it returns a Task you can `.step(...)`. A lint rule [no-coda-debug](/learn/linting/#no-coda-debug) flags every call so it doesn’t reach a commit.

Best on desktop edit & run it live Copy link

```ts
import { debug } from "@grlt-hub/app-coda"
import { compose, createTask, createWire, tag } from "@grlt-hub/app-compose"


const userId = tag<number>("userId")


const fetchUser = createTask({
  name: "fetch-user",
  run: { fn: () => ({ id: 1 }) },
})


const loadCart = createTask({
  name: "load-cart",
  run: {
    fn: () => {
      throw new Error("cart service is down")
    },
  },
})


compose()
  .step(fetchUser)
  .step(createWire({ from: fetchUser.result.id, to: userId }))
  // after fetchUser + wire
  .step(debug(fetchUser, userId))
  .step(loadCart)
  // after loadCart
  .step(debug(loadCart))
  .run()


console.warn("This sandbox flattens grouped logs.")
console.warn("In your DevTools console, debug() output is nested and collapsible.")
```

# Fallback on critical failures

> Run a backup path when a critical Task fails — by watching its status.

A *fallback Task* runs as a backup when a critical Task fails or is skipped. Its `enabled` reads any value that signals that — typically the critical Task’s `.status`, but a `.result` or a tag works just as well.

In the example, `dashboard` is the critical Task — it reads `auth.result`. If `auth` fails, `dashboard` is skipped. If `dashboard` itself fails, the result is the same: `dashboard.status` is not `"done"`, so `fallback` runs. Reading `.status` instead of `.result` is what keeps `fallback` enabled — status is always available, even after a skip.

Uncomment the `throw` to see it:

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask } from "@grlt-hub/app-compose"


const auth = createTask({
  name: "auth",
  run: {
    fn: () => {
      // uncomment to make auth fail
      // console shows "fallback shown" instead of "dashboard ready"
      // 👇
      // throw new Error("[auth]: failed")
      return { id: 1 }
    },
  },
})


// critical to the app
const dashboard = createTask({
  name: "dashboard",
  run: {
    context: auth.result,
    fn: (user) => console.log(`#${user.id} dashboard is ready`),
  },
})


// runs when dashboard doesn't reach "done"
const fallback = createTask({
  name: "fallback",
  run: { fn: () => console.log("fallback shown") },
  enabled: {
    context: dashboard.status,
    fn: (status) => status !== "done",
  },
})


compose()
  .step(auth)
  .step(dashboard)
  .step(fallback)
  .run()
```

For shorter `enabled` blocks, [`when`](/app-coda/when) from `@grlt-hub/app-coda` returns the `{ context, fn: Boolean }` pair in a single call.

# Inspecting your app

> Inspect your composition as JSON — log it, render it as a diagram, or snapshot it to catch unintended changes.

Your composition spans many files. `.graph()` returns it as a JSON tree — log it, render it as a diagram, feed it to an LLM. Or snapshot it to catch unintended changes.

## Inspecting

[Section titled “Inspecting”](#inspecting)

Call `.graph()` to get the tree without executing Tasks.

**Output:**

```json
{
  "type": "seq",
  "meta": {},
  "children": [
    {
      "type": "run",
      "meta": { "name": "alpha", "kind": "task" },
      "id": 0,
      "dependencies": { "required": [], "optional": [] }
    }
  ]
}
```

Best on desktop edit & run it live Copy link

```ts
import {
createWire, compose, tag, createTask, optional
} from "@grlt-hub/app-compose"


const title = tag<string>("title")


const alpha = createTask({
  name: "alpha",
  run: { fn: () => ({ list: [0], title: "hello" }) },
})


const beta = createTask({
  name: "beta",
  run: {
    context: { title: title.value },
    fn: console.log,
  },
})


const gamma = createTask({
  name: "gamma",
  run: {
    context: { list: optional(alpha.result.list) },
    fn: console.log,
  },
})


const app = compose()
  .step(alpha)
  .step([
    compose()
      .step(createWire({ from: alpha.result.title, to: title }))
      .step(beta),
    gamma,
  ])


const graph = app.graph()


console.log(JSON.stringify(graph, null, 2))
```

## Snapshotting the graph

[Section titled “Snapshotting the graph”](#snapshotting-the-graph)

Snapshot `.graph()` in a test to catch unintended changes.

```ts
import { describe, expect, it } from "vitest"
import { app } from "./app"


describe("app graph", () => {
  it("matches snapshot", () => {
    expect(app.graph()).toMatchSnapshot()
  })
})
```

# Testing your configuration

> Catch misconfiguration before runtime — duplicate Tasks, unwired Tags, unused Wires — with one assert in a test.

App-Compose checks the configuration on every `.run()`:

* the same Task added twice
* two Wires writing into the same Tag
* a Task reading from a Tag that’s never wired
* an unused Wire

“Same” means the same instance, not the same name.

`.guard()` runs the same check stricter, in a test: unused Wires throw instead of warning. All others throw in both.

Guard reports the first error it finds — fix and re-run to see the next.

## Writing the test

[Section titled “Writing the test”](#writing-the-test)

In any test framework, call `.guard()` and assert it doesn’t throw.

Best on desktop edit & run it live Copy link

```ts
import { createWire, literal, compose, tag, createTask } from "@grlt-hub/app-compose"


const apiUrl = tag("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(`token=${token}`),
  },
})


const orphanTag = tag("orphan")


const app = compose()
  // 👇 comment out — apiUrl never gets wired
  .step(createWire({ from: literal("<url>"), to: apiUrl }))


  // 👇 uncomment — apiUrl wired twice
  // .step(createWire({ from: literal("<other-url>"), to: apiUrl }))


  .step(auth)


  // 👇 uncomment — auth registered twice
  // .step(auth)


  // 👇 uncomment — orphan Wire (nothing reads it)
  // .step(createWire({ from: literal(null), to: orphanTag }))


  .step(dashboard)


describe("app configuration", () => {
  it("is valid", () => {
    expect(() => app.guard()).not.toThrow()
  })
})
```

# Managing Tags

> Find the right Tag count — reuse where meanings overlap, separate where they don't.

When creating Tags, it’s easy to slip into one of two extremes.

One Tag per Task — every shared value becomes a duplicate.

```ts
const dashboardTag = tag<{
  user: User
  widgets: Widget[]
}>("dashboard")


const dashboard = createTask({
  name: "dashboard",
  run: { context: dashboardTag.value, fn: () => {} },
})


const profileTag = tag<{
  user: User
  settings: Setting[]
}>("profile")


const profile = createTask({
  name: "profile",
  run: { context: profileTag.value, fn: () => {} },
})
```

One Tag per value — every new value means a new Tag and a new wire.

```ts
const dashboardUser = tag<User>("dashboard::user")
const dashboardWidgets = tag<Widget[]>("dashboard::widgets")


const dashboard = createTask({
  name: "dashboard",
  run: {
    context: { user: dashboardUser.value, widgets: dashboardWidgets.value },
    fn: () => {},
  },
})


const profileUser = tag<User>("profile::user")
const profileSettings = tag<Setting[]>("profile::settings")


const profile = createTask({
  name: "profile",
  run: {
    context: { user: profileUser.value, settings: profileSettings.value },
    fn: () => {},
  },
})
```

The right count sits between them.

A simple rule

Start with **one Tag per Task**. When two Tasks need the same value, create a **shared Tag**.

The hybrid: one shared Tag, separate Tags for what differs.

Best on desktop edit & run it live Copy link

```ts
import {
compose, createTask, createWire, tag, literal
} from "@grlt-hub/app-compose"


const userTag = tag<User>("user")


const dashboardTag = tag<{ widgets: Widget[] }>("dashboard")
const dashboard = createTask({
  name: "dashboard",
  run: {
    context: {
      user: userTag.value,
      widgets: dashboardTag.value.widgets,
    },
    fn: console.log,
  },
})


const profileTag = tag<{ settings: Setting[] }>("profile")
const profile = createTask({
  name: "profile",
  run: {
    context: {
      user: userTag.value,
      settings: profileTag.value.settings,
    },
    fn: console.log,
  },
})


compose()
  .step([
    createWire({ from: literal(1), to: userTag }),
    createWire({ from: { widgets: literal([]) }, to: dashboardTag }),
    createWire({ from: { settings: literal([]) }, to: profileTag }),
  ])
  .step([dashboard, profile])
  .run()


type User = unknown
type Widget = unknown
type Setting = unknown
```

## Deep context types

[Section titled “Deep context types”](#deep-context-types)

When a Task’s context type is deeply nested, a Tag often carries the whole type minus a few internal fields. Writing that subtype by hand is painful — and it goes stale the moment the source changes. Use `OmitDeep` from [`type-fest`](https://github.com/sindresorhus/type-fest).

```ts
import type { OmitDeep } from "type-fest"


type DashboardCtx = {
  user: { id: string }
  navigation: {
    sidebarApi: SidebarApi
    title: string
  }
  widgets: Widgets[]
}


const dashboardTag = tag<OmitDeep<DashboardCtx, "navigation.sidebarApi">>("dashboard")
// same as { user: { id: string }; navigation: { title: string }; widgets: Widgets[] }
```

# Mapping values

> Use shape to pick, derive, or merge values flowing between Tasks and Wires.

Values flow between Tasks and Wires. `shape` transforms a value between them — picking a piece of it, deriving a new one from it, or merging several into one. Its input is one or several sources. The transform lives in `shape`, not in `fn`.

## Inside a context

[Section titled “Inside a context”](#inside-a-context)

`shape` works in both `run.context` and `enabled.context`.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, shape } from "@grlt-hub/app-compose"


const user = createTask({
  name: "user",
  run: {
    fn: () => ({ name: "John", role: "admin" }),
  },
})


const greeting = createTask({
  name: "greeting",
  run: {
    // run.context
    context: {
      name: shape(user.result, (u) => u.name.toUpperCase()),
    },
    fn: ({ name }) => console.log(`Welcome back, ${name}`),
  },
})


const adminPanel = createTask({
  name: "admin-panel",
  run: {
    fn: () => console.log("Admin panel ready"),
  },
  enabled: {
    // enabled.context
    context: {
      isAdmin: shape(user.result, (u) => u.role === "admin"),
    },
    fn: ({ isAdmin }) => isAdmin,
  },
})


compose()
  .step(user)
  .step([greeting, adminPanel])
  .run()
```

## Inside a Wire

[Section titled “Inside a Wire”](#inside-a-wire)

`shape` works in `createWire.from`.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, createWire, shape, tag } from "@grlt-hub/app-compose"


const userName = tag<string>("userName")


const user = createTask({
  name: "user",
  run: {
    fn: () => ({ name: "John", role: "admin" }),
  },
})


const greeting = createTask({
  name: "greeting",
  run: {
    context: {
      name: userName.value,
    },
    fn: ({ name }) => console.log(`Welcome back, ${name}`),
  },
})


const userNameWire = createWire({
  from: shape(user.result, (u) => u.name.toUpperCase()),
  to: userName,
})


compose()
  .step(user)
  .step(userNameWire)
  .step(greeting)
  .run()
```

## Several sources

[Section titled “Several sources”](#several-sources)

Pass an array or an object instead of a single source — either works:

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, shape } from "@grlt-hub/app-compose"


const firstName = createTask({
  name: "first-name",
  run: { fn: () => "John" },
})


const lastName = createTask({
  name: "last-name",
  run: { fn: () => "Doe" },
})


const fullNameShape = shape(
  {
    first: firstName.result,
    last: lastName.result,
  },
  (v) => `${v.first} ${v.last}`,
)


// as an array
// shape(
//   [firstName.result, lastName.result],
//   ([first, last]) => `${first} ${last}`,
// )


const fullName = createTask({
  name: "full-name",
  run: {
    context: { fullName: fullNameShape },
    fn: console.log,
  },
})


compose()
  .step([firstName, lastName])
  .step(fullName)
  .run()
```

## Failure behavior

[Section titled “Failure behavior”](#failure-behavior)

When `shape` fails, the downstream Task skips. Two cases:

* The source Task fails
* The `shape` callback throws an error

The rest of the composition runs as usual.

# Organizing your composition

> Group related work into a nested compose block — name reusable parts and match the layout to the structure.

A flat chain of `.step(...)` works for a small app. Once it grows, the layout stops reflecting the structure:

```ts
compose()
  .step(createWire({ from: literal("https://api"), to: apiUrl }))
  .step([auth, loadTranslations])
  .step([fetchUser, fetchPermissions])
  .step(dashboard)
  .run()
```

The shape carries order — nothing about what belongs to what. The Wire for `apiUrl` and the `auth` Task that reads it are in different steps. `auth` shares its step with `loadTranslations`, which isn’t part of `auth`. The auth chain spreads across three steps; reusing it elsewhere means copying lines.

## Nesting

[Section titled “Nesting”](#nesting)

A `.step(...)` also accepts another `compose()` — group related work into a block, mix sequential and parallel in one step, name reusable parts of your app.

```ts
compose()
  .step([
    loadTranslations,
    compose()
      .step(createWire({ from: literal("https://api"), to: apiUrl }))
      .step(auth)
      .step([fetchUser, fetchPermissions]),
  ])
  .step(dashboard)
  .run()
```

Only the outer `compose()` calls `.run()` — it runs everything inside.

## Presets

[Section titled “Presets”](#presets)

A preset is a named `compose()` — a variable when no arguments are needed, a function when they are.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, tag, createWire, literal } from "@grlt-hub/app-compose"


/** preset */


const apiUrl = tag<string>("api-url")


const auth = createTask({
  name: "auth",
  run: {
    context: { url: apiUrl.value },
    fn: () => ({ id: 1 }),
  },
})


const fetchUser = createTask({
  name: "fetch-user",
  run: {
    context: { id: auth.result.id },
    fn: (ctx) => {
      console.log(`User: id=${ctx.id}; name="John"`)
    },
  },
})


const fetchPermissions = createTask({
  name: "fetch-permissions",
  run: {
    context: { id: auth.result.id },
    fn: () => {
      console.log(`Permissions fetched`)
    },
  },
})


// or as a function: (url: string) => compose()...
const authPreset = compose()
  .step(createWire({ from: literal("https://api"), to: apiUrl }))
  .step(auth)
  .step([fetchUser, fetchPermissions])


/** composition */


const loadTranslations = createTask({
  name: "load-translations",
  run: {
    fn: () => console.log(`Translations fetched`),
  },
})


const dashboard = createTask({
  name: "dashboard",
  run: { fn: () => console.log("Dashboard ready") },
})


compose().step([loadTranslations, authPreset]).step(dashboard).run()
```

# Observing your composition

> Attach lifecycle hooks to a compose chain — onStart, onComplete, onTaskFail — for logs and metrics.

A compose chain emits lifecycle events: start, complete, task failure. `.meta({ name, hooks })` attaches callbacks for each: `onStart`, `onComplete`, `onTaskFail`. The `name` identifies each chain in logs and metrics.

## Hooks

[Section titled “Hooks”](#hooks)

### onStart

[Section titled “onStart”](#onstart)

Fires when the compose chain starts.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask } from "@grlt-hub/app-compose"


const auth = createTask({
  name: "auth",
  run: {
    fn: () => {
      console.log("auth ran")
      return { id: 1 }
    },
  },
})


compose()
  .meta({
    name: "auth-page",
    hooks: {
      onStart: (event) => {
        console.log(`${event.meta?.name} started`)
      },
    },
  })
  .step(auth)
  .run()
```

### onComplete

[Section titled “onComplete”](#oncomplete)

Fires when the compose chain completes.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask } from "@grlt-hub/app-compose"


const auth = createTask({
  name: "auth",
  run: {
    fn: () => {
      console.log("auth ran")
      return { id: 1 }
    },
  },
})


compose()
  .meta({
    name: "auth-page",
    hooks: {
      onComplete: (event) => {
        console.log(`${event.meta?.name} completed`)
      },
    },
  })
  .step(auth)
  .run()
```

### onTaskFail

[Section titled “onTaskFail”](#ontaskfail)

Fires when a task inside the compose chain fails.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask } from "@grlt-hub/app-compose"


const auth = createTask({
  name: "auth",
  run: {
    fn: () => {
      throw new Error("Ooops!")
    },
  },
})


compose()
  .meta({
    name: "auth-page",
    hooks: {
      onTaskFail: (event) => {
        console.log(`Task ${event.task.name} failed`)
        console.log(`With an error: ${event.error}`)
      },
    },
  })
  .step(auth)
  .run()
```

## Nested compose

[Section titled “Nested compose”](#nested-compose)

### Event bubbling

[Section titled “Event bubbling”](#event-bubbling)

# Optional values

> Mark a value as not required — wherever it flows between Tasks and Wires. The Task still runs when the source is missing.

Values flow between Tasks and Wires. Every value a Task reads is required by default. `optional` marks one as not required — when the source is missing, the Task still runs and the field is `undefined`.

## Inside a context

[Section titled “Inside a context”](#inside-a-context)

`optional` works in both `run.context` and `enabled.context`.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, optional } from "@grlt-hub/app-compose"


const user = createTask({
  name: "user",
  run: {
    fn: () => ({ name: "John" }),
  },
})


const location = createTask({
  name: "location",
  run: { fn: () => "Antarctica" },
})


const greeting = createTask({
  name: "greeting",
  run: {
    context: { user: optional(user.result) }, // 👈
    fn: ({ user }) => {
      console.log(`Hello, ${user?.name ?? "<unknown-user>"}`)
    },
  },
})


const recommendations = createTask({
  name: "recommendations",
  run: {
    fn: () => console.log("recommendations loaded"),
  },
  enabled: {
    context: [
      optional(user.result), // 👈
      optional(location.result), // 👈
    ],
    fn: (ctx) => ctx.some((x) => x !== undefined),
  },
})


// Try commenting a step:
//   .step(user)      → "<unknown-user>", recommendations run
//   .step(location)  → "Hello, John", recommendations run
//   both             → "<unknown-user>", recommendations skip
compose()
  .step(user)
  .step(greeting)
  .step(location)
  .step(recommendations)
  .run()
```

## Inside a Wire

[Section titled “Inside a Wire”](#inside-a-wire)

`optional` works in `createWire.from`. The destination tag carries `T | undefined`.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, createWire, optional, tag } from "@grlt-hub/app-compose"


const userName = tag<string | undefined>("userName")


const user = createTask({
  name: "user",
  run: {
    fn: () => ({ name: "John" }),
  },
})


const greeting = createTask({
  name: "greeting",
  run: {
    context: {
      name: userName.value,
    },
    fn: ({ name }) => {
      console.log(`Welcome back, ${name ?? "<unknown-user>"}`)
    },
  },
})


const userNameWire = createWire({
  from: optional(user.result.name), // 👈
  to: userName,
})


compose()
  // 👇 comment — greeting → "<unknown-user>"
  .step(user)
  .step(userNameWire)
  .step(greeting)
  .run()
```

## Inside a shape

[Section titled “Inside a shape”](#inside-a-shape)

`optional` works in `shape`. The callback receives `T | undefined`.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, createWire, optional, shape, tag } from "@grlt-hub/app-compose"


const userName = tag<string>("userName")


const user = createTask({
  name: "user",
  run: { fn: () => ({ name: "John" }) },
})


const greeting = createTask({
  name: "greeting",
  run: {
    context: {
      name: userName.value,
    },
    fn: ({ name }) => console.log(`Welcome back, ${name}`),
  },
})


const userNameShape = shape(
  optional(user.result.name), // 👈
  (name) => name ?? "<unknown-user>"
)


const userNameWire = createWire({
  from: userNameShape,
  to: userName,
})


compose()
  // 👇 comment — greeting → "<unknown-user>"
  .step(user)
  .step(userNameWire)
  .step(greeting)
  .run()
```

# Composing a React app

> Compose a React app from independent features — each feature is its own Task.

In a React app, each feature — or a group of features — is a Task. A Task can return anything — this example uses `{ ui, api }`.

One Task at the end — a *Render Task* — mounts the UI into the page. It usually reads upstream Tasks directly: every Task is known here, so a Tag adds nothing.

**What the example does:**

* `ThemeSelect` is a feature — it returns the chosen theme and a way to choose it.
* `LanguageSelect` is a feature — it returns the chosen language and a way to choose it.
* `Appearance` is a layout — through its context it reads the language from a shared Tag and the widgets from another Tag.
* `ThemeProvider` is a provider — through its context it reads the chosen theme.
* The Render Task renders the `Layout` from `Appearance` wrapped in the `Provider` from `ThemeProvider`.
* `compose()` wires it all; the Render Task runs last.

[nanostores](https://github.com/nanostores/nanostores) here is only for the `task.api` demo. Features can use any state manager.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, createWire, literal } from "@grlt-hub/app-compose"
import { createRoot } from "react-dom/client"
import { Appearance, appearanceWidgets } from "./appearance"
import { LanguageSelect, ThemeSelect } from "./features"
import { selectedTheme, selectedLanguage } from "./shared-tags"
import { ThemeProvider } from "./theme-provider"


// page-level — reads Tasks directly
const renderApp = createTask({
  name: "render-app",
  run: {
    context: {
      AppearanceLayout: Appearance.result.ui.Layout,
      ThemeProvider: ThemeProvider.result.ui.Provider,
    },
    fn: (ctx) => {
      const root = createRoot(document.getElementById("root"))
      root.render(
        <ctx.ThemeProvider>
          <ctx.AppearanceLayout />
        </ctx.ThemeProvider>,
      )
    },
  },
})


compose()
  .step([ThemeSelect, LanguageSelect])
  // fill features data into shared tags
  .step([
    createWire({
      from: {
        theme: ThemeSelect.result.api.$theme,
        language: LanguageSelect.result.api.$language,
      },
      to: { theme: selectedTheme, language: selectedLanguage },
    }),
  ])
  // collect feature widgets into one tag
  .step(
    createWire({
      from: [
        {
          Widget: ThemeSelect.result.ui.Widget,
          key: literal(ThemeSelect.name),
        },
        {
          Widget: LanguageSelect.result.ui.Widget,
          key: literal(LanguageSelect.name),
        },
      ],
      to: appearanceWidgets,
    }),
  )
  .step([Appearance, ThemeProvider])
  .step(renderApp)
  .run()
```

# Composing a Vue app

> Compose a Vue app from independent features — each feature is its own Task.

In a Vue app, each feature — or a group of features — is a Task. A Task can return anything — this example uses `{ ui, api }`.

Each feature is split into a View (`.vue` SFC) and a Task: the View takes the feature’s atom as a prop and stays a plain SFC; the Task creates the atom, wraps the View into a `Widget` closure that supplies it, and returns both `ui.Widget` and `api.$atom`.

One Task at the end — a *Render Task* — mounts the UI into the page. It reads upstream Tasks directly: every Task is known here, so a Tag adds nothing.

`App.vue` only hosts the mount target (`<div id="root">`) and runs `compose()`.

**What the example does:**

* `ThemeSelect` is a feature — it returns the chosen theme and a way to choose it.
* `LanguageSelect` is a feature — it returns the chosen language and a way to choose it.
* `Appearance` is a layout — through its context it reads the language from a shared Tag and the widgets from another Tag.
* `ThemeProvider` is a provider — through its context it reads the chosen theme.
* The Render Task mounts the `Layout` from `Appearance` wrapped in the `Provider` from `ThemeProvider`.
* `compose()` wires it all; the Render Task runs last.

[nanostores](https://github.com/nanostores/nanostores) here is only for the `task.api` demo. Features can use any state manager.

Best on desktop edit & run it live Copy link

```ts
<script setup>
import { compose, createTask, createWire, literal } from "@grlt-hub/app-compose"
import { createApp, h, onMounted } from "vue"
import { Appearance, appearanceWidgets } from "./appearance.js"
import { LanguageSelect, ThemeSelect } from "./features.js"
import { selectedLanguage, selectedTheme } from "./shared-tags.js"
import { ThemeProvider } from "./theme-provider.js"


// page-level — reads Tasks directly
const renderApp = createTask({
  name: "render-app",
  run: {
    context: {
      AppearanceLayout: Appearance.result.ui.Layout,
      ThemeProvider: ThemeProvider.result.ui.Provider,
    },
    fn: (ctx) => {
      createApp({
        render: () => h(ctx.ThemeProvider, null, () => h(ctx.AppearanceLayout)),
      }).mount("#root")
    },
  },
})


onMounted(() => {
  compose()
    .step([ThemeSelect, LanguageSelect])
    // fill features data into shared tags
    .step([
      createWire({
        from: {
          theme: ThemeSelect.result.api.$theme,
          language: LanguageSelect.result.api.$language,
        },
        to: { theme: selectedTheme, language: selectedLanguage },
      }),
    ])
    // collect feature widgets into one tag
    .step(
      createWire({
        from: [
          { Widget: ThemeSelect.result.ui.Widget, key: literal(ThemeSelect.name) },
          { Widget: LanguageSelect.result.ui.Widget, key: literal(LanguageSelect.name) },
        ],
        to: appearanceWidgets,
      }),
    )
    .step([Appearance, ThemeProvider])
    .step(renderApp)
    .run()
})
</script>


<template>
  <div id="root"></div>
</template>
```

# AI tools

> Plug App-Compose docs into Cursor, Claude, ChatGPT, Copilot, or DeepWiki via llms.txt.

App-Compose publishes LLM-friendly docs and is available on [DeepWiki](https://deepwiki.com/grlt-hub/app-compose).

## 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 URLs 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).

## DeepWiki

[Section titled “DeepWiki”](#deepwiki)

Open [deepwiki.com/grlt-hub/app-compose](https://deepwiki.com/grlt-hub/app-compose) and ask anything about the codebase.

# Installation

> Install @grlt-hub/app-compose with your package manager — or try it in the sandbox without installing.

App-Compose has zero dependencies. Written in TypeScript — no `@types` package needed.

## Try App-Compose

[Section titled “Try App-Compose”](#try-app-compose)

You don’t need to install anything to try App-Compose. Open 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
  ```

# Linting

> Enforce App-Compose conventions with the ESLint plugin.

An ESLint plugin enforces App-Compose conventions in TypeScript code. It requires ESLint 9+.

## 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"
import tseslint from "typescript-eslint"


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)

### no-async-shape-callback

[Section titled “no-async-shape-callback”](#no-async-shape-callback)

❗ This rule errors in the `recommended` config.

💭 This rule requires [typed linting](https://typescript-eslint.io/getting-started/typed-linting/).

***

In a `shape()` callback, async work runs outside the chain, and App-Compose cannot order it. The plugin reports every callback that returns a `Promise`.

```ts
// ❌ Wrong
shape(user.result, async (u) => await fetchAvatar(u.id))


// ✅ Correct
shape(fetchAvatar.result, (a) => a.url)
```

### no-coda-debug

[Section titled “no-coda-debug”](#no-coda-debug)

❗ This rule errors in the `recommended` config.

***

The `debug()` helper from `@grlt-hub/app-coda` prints task state to the console. The plugin reports every call to it, so it stays out of committed code.

```ts
import { compose } from "@grlt-hub/app-compose"
import { debug } from "@grlt-hub/app-coda"


// ❌ Wrong
compose().step(login).step(debug(login)).run()


// ✅ Correct
compose().step(login).run()
```

### task-options-order

[Section titled “task-options-order”](#task-options-order)

⚠️ This rule warns in the `recommended` config.

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

***

This order matches how a Task reads top-to-bottom: `name` first, then `run` before the optional `enabled`. Inside both, `context` comes before `fn`. The plugin reports when the order differs.

```ts
// ❌ Wrong
createTask({
  name: "alpha",
  enabled: {
    fn: check,
    context: { authorized: authorizedTag },
  },
  run: {
    context: { timeout: timeoutTag },
    fn: init,
  },
})


// ✅ Correct
createTask({
  name: "alpha",
  run: {
    context: { timeout: timeoutTag },
    fn: init,
  },
  enabled: {
    context: { authorized: authorizedTag },
    fn: check,
  },
})
```

### wire-options-order

[Section titled “wire-options-order”](#wire-options-order)

⚠️ This rule warns in the `recommended` config.

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

***

A Wire carries data from a source to a destination tag, so `from` is listed before `to`. The plugin reports when the order differs.

```ts
// ❌ Wrong
createWire({
  to: apiUrl,
  from: literal("https://api.example.com"),
})


// ✅ Correct
createWire({
  from: literal("https://api.example.com"),
  to: apiUrl,
})
```

# Quick Start

> Walk through App-Compose in five steps — create a Task, run it, pass data, disable it, handle failures.

App-Compose is built on the three key pieces: *Task*, *Tag*, and *Wire*. A Task does the work. A Tag holds a value. A Wire supplies it.

No magic, no copy-paste, no surprises at startup — everything else flows from these three. Five short steps follow.

You will learn

* How to create a Task
* How to run a Task
* How to pass data to a Task
* How to disable a Task
* When a Task fails or is skipped

## How to create a Task

[Section titled “How to create a Task”](#how-to-create-a-task)

A Task is one unit of work — it has a `name` and a function to `run`. A Task can be as small as a single API request, or as large as a product feature.

```ts
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 required 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)

To run a Task, place it in a *Step*. A Step is a position in the run order. Steps let you say what runs before what, and what runs together.

Steps follow two rules:

1. **Each `.step()` runs after the previous one.** `.step(a).step(b)` — first `a`, then `b`.
2. **Passing an array runs Tasks in parallel.** `.step([a, b])` — `a` and `b` run at the same time.

```js
.step(alpha)
.step([beta, gamma])
.step(delta)


// is equivalent to:
alpha()
  .then(() => Promise.all([beta(), gamma()]))
  .then(() => delta())
```

Once all the Steps are listed, all that’s left is to call `run()`.

Best on desktop edit & run it live Copy link

```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()
  // 👇 define
  .step(fetchUser)
  // 👇 run
  .run()
```



## How to pass data to a Task

[Section titled “How to pass data to a Task”](#how-to-pass-data-to-a-task)

Most Tasks need data from outside — a username, a product ID, the current locale. To stay reusable, they read it through a *Context* (a property of the Task config) instead of hardcoding values. `run.fn` receives whatever `run.context` resolves to.

```ts
const fetchUser = createTask({
  name: "fetch-user",
  run: {
    context: { id: userId.value },
    fn: (ctx) => console.log(`Fetching user ${ctx.id}`),
  },
})
```

There are two ways to fill it.

### Through a Tag

[Section titled “Through a Tag”](#through-a-tag)

A *Tag* is a placeholder for data. A Task reads it through `tag.value`. A `createWire` supplies that value — so the Task never knows where it came from.

```ts
.step(logIn)
.step(createWire({ from: logIn.result.id, to: userId }))
.step(fetchUser)


// is equivalent to:
[ logIn.result.id ] => [ userId ] => [ fetchUser reads userId.value ]
      (source)            (Tag)                (reader)
```



Have a look at the result:

Best on desktop edit & run it live Copy link

```ts
import { createWire, compose, tag, createTask } from "@grlt-hub/app-compose"


const userId = tag<number>("userId")


const fetchUser = createTask({
  name: "fetch-user",
  run: {
    context: { userId: userId.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()
  .step(logIn)
  .step(createWire({ from: logIn.result.id, to: userId }))
  .step(fetchUser)
  .run()
```

For values you already know upfront — a primitive or object — wrap them in `literal`. That tells App-Compose the value is data, not a reference to another Task or Tag.

```ts
import { literal } from "@grlt-hub/app-compose"


.step(createWire({ from: literal(42), to: userId }))
```

The path is the same — only the source changes.

Caution

Keep `createWire` and Tasks in separate `.step()` calls. If they share a Step, App-Compose throws an error.

```ts
// ❌ Wrong
.step([createWire(...), task])


// ✅ Correct
.step(createWire(...)).step(task)


// ✅ Correct
.step(
  compose().step(createWire(...)).step(task)
)
```

### Task to Task

[Section titled “Task to Task”](#task-to-task)

One Task reads another’s result directly — no Tag, no Wire. The two are now coupled. Reasonable when they always run together; otherwise, prefer a Tag.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask } from "@grlt-hub/app-compose"


const logIn = createTask({
  name: "log-in",
  run: {
    fn: () => ({ userId: 1 }),
  },
})


const fetchUser = createTask({
  name: "fetch-user",
  run: {
    context: {
      // 👇 directly coupled to logIn
      userId: logIn.result.userId,
    },
    fn: async (ctx) => {
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${ctx.userId}`)
      const result = await response.json()
      console.log(JSON.stringify(result, null, 2))
    },
  },
})


compose()
  .step(logIn)
  .step(fetchUser)
  .run()
```

Caution

Coupled Tasks need separate `.step()` calls. In `.step([logIn, fetchUser])` they run in parallel — `fetchUser` tries to read `logIn`’s result before it exists. App-Compose throws an error.

```ts
// ❌ Wrong
.step([logIn, fetchUser])


// ✅ Correct
.step(logIn).step(fetchUser)
```

## How to disable a Task

[Section titled “How to disable a Task”](#how-to-disable-a-task)

Every Task is enabled by default. To skip one based on a feature flag, the current user, or any other state — add an `enabled` block next to `run`.

`enabled` has the same shape as `run`, but `enabled.fn` returns `boolean | Promise<boolean>`. A `false` result skips the Task.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask } from "@grlt-hub/app-compose"


const featureFlags = createTask({
  name: "feature-flags",
  run: {
    fn: () => {
      // 👇 true = Task runs, false = Task is skipped
      const flags = { logInAllowed: true }
      console.log(JSON.stringify(flags, undefined, 2))


      return flags
    },
  },
})


const logIn = createTask({
  name: "log-in",
  run: {
    fn: () => console.log("[log-in]: user logged"),
  },
  enabled: {
    context: featureFlags.result.logInAllowed,
    // 👇 Return false to skip this Task
    fn: (enabled) => enabled,
  },
})


compose()
  .step(featureFlags)
  .step(logIn)
  .run()
```

## When a Task fails or is skipped

[Section titled “When a Task fails or is skipped”](#when-a-task-fails-or-is-skipped)

When a Task fails or is skipped, every Task that reads from it is **skipped** too. The path doesn’t matter — Tag or direct, the result is the same. Your app keeps running.

Best on desktop edit & run it live Copy link

```ts
import { compose, createTask, createWire, tag } from "@grlt-hub/app-compose"


const userId = tag<number>("userId")


const logIn = createTask({
  name: "log-in",
  run: {
    fn: () => {
      // 👇 uncomment to make logIn fail
      // throw new Error("oops")
      return { userId: 1 }
    },
  },
  // 👇 or uncomment to skip logIn instead
  // enabled: { fn: () => false },
})


// reads through a Tag
const fetchUser = createTask({
  name: "fetch-user",
  run: {
    context: userId.value,
    fn: () => {},
  },
})


// reads directly
const loadCart = createTask({
  name: "load-cart",
  run: {
    context: logIn.result.userId,
    fn: () => {},
  },
})


// independent — runs no matter what
const sendAnalytics = createTask({
  name: "send-analytics",
  run: { fn: () => {} },
})


// 👇 prints every task's status
const debug = createTask({
  name: "debug",
  run: {
    context: {
      [logIn.name]: logIn.status,
      [fetchUser.name]: fetchUser.status,
      [loadCart.name]: loadCart.status,
      [sendAnalytics.name]: sendAnalytics.status,
    },
    fn: (ctx) => {
      Object.entries(ctx).forEach(([name, status]) => {
        console.log(`${name} status: ${status}`)
      })
    },
  },
})


compose()
  .step(logIn)
  .step(createWire({ from: logIn.result.userId, to: userId }))
  .step([fetchUser, loadCart, sendAnalytics])
  .step(debug)
  .run()
```

An [optional](/guides/optional) modifier lets a Task keep running even when its source fails or skips. That’s a topic for the guides.

## Next Steps

[Section titled “Next Steps”](#next-steps)

The guides cover more. But first, set up your project — head to **[Installation](/learn/installation/)**.

# Using TypeScript

> Type your App-Compose code so TypeScript catches mismatches on supply and read.

App-Compose is written in TypeScript. This page shows how to type your code.

For the full list of exported types, see [reference/types](/reference/types)

## Typing a Tag

[Section titled “Typing a Tag”](#typing-a-tag)

A Tag holds a typed value. TypeScript then catches mismatches on both ends — supply and read.

Best on desktop edit & run it live Copy link

```ts
import { tag } from "@grlt-hub/app-compose"


const title = tag<string>("title")
```

# Overview

> Public API reference for App-Compose — one page per function, type, or helper.

Each page documents one piece of the public API. Use it to look something up, not to learn a pattern.

Missing one? [Tell us](/community/) or send a PR.

# compose

> Creates a Composer — chains Tasks and Wires through .step(), then runs, inspects, or guards them.

`compose()` returns a Composer. `.step()` adds Tasks and Wires; `.run()`, `.graph()`, and `.guard()` consume the chain.

## graph

[Section titled “graph”](#graph)

Returns the chain as a tree without executing it.

* `.graph()` — Returns a JSON-serializable tree
* Each node has a `type`: `"seq"`, `"con"`, or `"run"`
* `seq` — Sequential group; children run one after another. The tree’s root is always a `seq`
* `con` — Concurrent group; children run in parallel
* Both `seq` and `con` have `meta.name` (from `.meta({ name })`, or `undefined`) and `children`
* `children` may contain `run`, `seq`, or `con` nodes; a nested `compose()` appears as a nested `seq`
* `run` — Leaf for a Task or Wire with `meta.name` (Task name, or destination Tag names joined with `+` for a Wire), `meta.kind` (`"task"` or `"wire"`), `id`, and `dependencies`
* `id` — Increments in tree-traversal order from `0`
* `dependencies` — `{ required: number[], optional: number[] }`. IDs of `run` nodes this leaf reads from. In `required`, an entry of `-1` means no earlier `run` produces that value; `optional` omits missing producers

```ts
compose()
  .meta({ name: "app" })
  .step(createWire({ from: literal(1), to: userId }))
  .step([fetchUser, fetchPermissions])
  .step(compose().meta({ name: "cleanup" }).step(close))
  .graph()
```

```json
{
  "type": "seq",
  "meta": { "name": "app" },
  "children": [
    {
      "type": "run",
      "meta": { "name": "userId", "kind": "wire" },
      "id": 0,
      "dependencies": { "required": [], "optional": [] }
    },
    {
      "type": "con",
      "meta": {},
      "children": [
        {
          "type": "run",
          "meta": { "name": "fetchUser", "kind": "task" },
          "id": 1,
          "dependencies": { "required": [0], "optional": [] }
        },
        {
          "type": "run",
          "meta": { "name": "fetchPermissions", "kind": "task" },
          "id": 2,
          "dependencies": { "required": [0], "optional": [] }
        }
      ]
    },
    {
      "type": "seq",
      "meta": { "name": "cleanup" },
      "children": [
        {
          "type": "run",
          "meta": { "name": "close", "kind": "task" },
          "id": 3,
          "dependencies": { "required": [], "optional": [] }
        }
      ]
    }
  ]
}
```

## guard

[Section titled “guard”](#guard)

Checks the chain configuration without executing it.

* `.guard()` — Returns nothing; throws on the first problem found
* Duplicate Task or Wire — same instance added twice
* Missing context — a Spot has no producer in the chain
* Unused Wire — its value is never read

```ts
expect(() => app.guard()).not.toThrow()
```

Same checks run inside `.run()`, where unused Wires log a warning via `console.warn` instead of throwing.

## meta

[Section titled “meta”](#meta)

Attaches metadata to the chain: a `name` and lifecycle hooks.

* `.meta({ name?, hooks? })` — Updates `name` and/or `hooks` on the chain
* `name` — String identifier. Appears in `.graph()` output, in messages from the guard, and in `onStart`/`onComplete` payloads
* `hooks.onStart({ meta })` — Fires when the chain starts; receives the chain’s meta
* `hooks.onComplete({ meta })` — Fires when the chain completes; receives the chain’s meta
* `hooks.onTaskFail({ task, error })` — Fires when a Task inside the chain fails; receives the failing Task and its error
* Returns a Composer

```ts
compose()
  .meta({
    name: "app",
    hooks: {
      onStart: ({ meta }) => console.log(`${meta?.name}: start`),
      onComplete: ({ meta }) => console.log(`${meta?.name}: complete`),
      onTaskFail: ({ task, error }) => console.error(`${task.name}:`, error),
    },
  })
  .step(loadUser)
```

## run

[Section titled “run”](#run)

Executes the chain.

* `.run()` — Returns `Promise<Scope>`
* `scope.get(spot)` — Returns the resolved value of `spot`, or `undefined`

```ts
const scope = await compose()
  .step(loadUser)
  .step(createWire({ from: loadUser.result.id, to: userId }))
  .run()


scope.get(loadUser.result) // { id, name }
scope.get(loadUser.status) // "done"
scope.get(userId.value) // the id
```

Before execution, `.run()` runs the guard. Duplicate Tasks or Wires and missing context throw; unused Wires log a warning via `console.warn`. Use `.guard()` for a strict variant where unused Wires also throw.

`.get` returns `undefined` when the Spot has no value — a Task’s `result` after it failed or was skipped, or a Tag that no Wire has supplied.

## step

[Section titled “step”](#step)

Appends a Task, Wire, or nested Composer to the chain.

* `.step(child)` — Appends `child` as the next sequential step
* `.step([...children])` — Appends an array as a concurrent group. Members start together
* `child`, array member — A Task, a Wire, or another Composer
* Returns a Composer
* Throws when the argument is anything else

```ts
compose()
  .step(loadConfig)
  .step([fetchUser, fetchPermissions])
  .step(createWire({ from: literal("/api"), to: apiUrl }))
  .step(compose().step(setup).step(start))
  .step([compose().step(dashboard), compose().step(analytics)])
```

Each `.step()` runs after the previous one completes; later steps can read values produced by earlier steps. Members of a concurrent group cannot read each other’s values.

# createTask

> Creates a Task — one unit of work with a name, a run function, and optional inputs.

Creates a Task — one unit of work with a `name` and a `run.fn`.

* `createTask(config)` — Returns a Task
* `config.name` — Required string used in errors and `task.name`
* `config.run.fn` — Required function; receives `config.run.context` resolved, returns the Task’s value. Sync or async
* `config.run.context` — Optional `Spot`, or record or array of Spots
* `config.enabled.fn` — Optional function; receives `config.enabled.context` resolved, returns a boolean. `false` skips the Task. Sync or async
* `config.enabled.context` — Optional `Spot`, or record or array of Spots
* `task.name` — Mirror of `config.name`
* `task.kind` — Literal string `"task"`
* `task.result` — What `run.fn` returns
* `task.status` — `Spot<"done" | "fail" | "skip">`
* `task.error` — `Spot<unknown>`; set when status is `"fail"`

```ts
createTask({
  name: "ping",
  run: { fn: () => "pong" },
})


createTask({
  name: "fetchUser",
  run: {
    context: userId.value,
    fn: (id) => fetch(`/users/${id}`).then((r) => r.json()),
  },
})


createTask({
  name: "loadProfile",
  run: { context: user.result, fn: (u) => u.profile },
  enabled: { context: featureFlag.value, fn: (f) => f.profile },
})
```

Status is `"fail"` when `run.fn` or `enabled.fn` throws, `"skip"` when `enabled.fn` returns `false` or any required context is missing, `"done"` otherwise.

# createWire

> Builds a Wire — supplies a value to a Tag so Tasks reading the Tag can use it.

Supplies a value to a Tag.

* `createWire(config)` — Returns a Wire
* `config.from` — A `Spot`, or a record or array of Spots
* `config.to` — A Tag, or a record or array of Tags; must match `from`’s shape
* `wire.name` — Destination Tag names joined with `+`
* `wire.kind` — Literal string `"wire"`

```ts
const userId = tag<number>("userId")


createWire({ from: user.result.id, to: userId })


const userRole = tag<number>("userRole")


createWire({
  from: { id: user.result.id, role: user.result.role },
  to: { id: userId, role: userRole },
})


createWire({
  from: [user.result.id, user.result.role],
  to: [userId, userRole],
})
```

# is

> Runtime checks for App-Compose units.

Runtime checks for App-Compose units.

* `is.tag` — Predicate for values returned by `tag(...)`
* `is.task` — Predicate for values returned by `createTask(...)`
* `is.wire` — Predicate for values returned by `createWire(...)`
* `is.runnable` — Predicate for any value returned by `createTask(...)` or `createWire(...)`

Best on desktop edit & run it live Copy link

```ts
const apiUrl = tag("apiUrl")


const auth = createTask({
  name: "auth",
  run: { fn: () => {} },
})


const apiUrlWire = createWire({
  from: literal(""),
  to: apiUrl,
})


describe("is", () => {
  it("is.tag", () => {
    expect(is.tag(apiUrl)).toBeTruthy()
    expect(is.tag(auth)).toBeFalsy()
    expect(is.tag(apiUrlWire)).toBeFalsy()
  })


  it("is.task", () => {
    expect(is.task(auth)).toBeTruthy()
    expect(is.task(apiUrl)).toBeFalsy()
    expect(is.task(apiUrlWire)).toBeFalsy()
  })


  it("is.wire", () => {
    expect(is.wire(apiUrlWire)).toBeTruthy()
    expect(is.wire(auth)).toBeFalsy()
    expect(is.wire(apiUrl)).toBeFalsy()
  })


  it("is.runnable", () => {
    expect(is.runnable(apiUrlWire)).toBeTruthy()
    expect(is.runnable(auth)).toBeTruthy()
    expect(is.runnable(apiUrl)).toBeFalsy()
  })
})
```

# literal

> Wraps a known value (primitive or object) so a Wire or Task can use it as a source.

Wraps a value as data — not a reference to a Task or Tag.

`literal<const T>(value)` — Returns a `Spot<T>`. Use for values you already know upfront — a primitive or object

```ts
literal(42)
literal("https://api.example.com")
literal({ retries: 3, timeout: 1000 })
```

# optional

> Marks a value as not required — works inside run.context, enabled.context, createWire.from, and shape.

Marks a value as not required — the Task still runs when the source is missing.

`optional<T>(spot)` — Wraps a `Spot<T>` and returns a `Spot<T | undefined>`. Works inside `run.context`, `enabled.context`, `createWire.from`, and `shape`

```ts
optional(user.result)
optional(myTag.value)
```

# shape

> Transforms one or several source values into a new one — inside run.context, enabled.context, or a Wire.

Transforms one or several Spots into a new value. Works inside `run.context`, `enabled.context`, and `createWire.from`.

* `shape<S, T>(spot, fn)` — Returns a `Spot<T>`. `fn` receives the value of `spot`
* `shape<S, T>(sources, fn)` — Returns a `Spot<T>` from a record or array of `Spot`s. `fn` receives an object or array of their values
* `shape<T>(sources)` — Returns a `Spot<T>` without remapping the value of record or array of `Spot`s, only grouping them into one `Spot`.

```ts
shape(user.result, (u) => u.name.toUpperCase())


shape({ first: firstName.result, last: lastName.result }, (v) => `${v.first} ${v.last}`)


shape([firstName.result, lastName.result], ([first, last]) => `${first} ${last}`)


shape({ first: firstName.result, last: lastName.result }) // Spot<{ first: string; last: string }>
```

When `shape` fails, the downstream Task skips — the source Task fails, or the callback throws

# tag

> Creates a Tag — a typed, named placeholder that a Wire supplies and a Task reads.

Creates a typed placeholder for a value that a Wire supplies and a Task reads.

* `tag<T>(name)` — Returns a Tag of value type `T` (default `never`). `name` is a debug string used in error messages and Wire names
* `tag.name` — The string passed to `tag(...)`
* `tag.value` — Typed reference to the Tag’s value, used inside `run.context`

```ts
const userId = tag<number>("userId")


userId.name // "userId"
userId.value // Spot<number>
```

# Exported Types

> Public TypeScript types exported from App-Compose.

Types exported from `@grlt-hub/app-compose`

## ComposeHookMap

[Section titled “ComposeHookMap”](#composehookmap)

Type for compose lifecycle hooks: `onStart`, `onComplete`, `onTaskFail`.

Best on desktop edit & run it live Copy link

```ts
import { type ComposeHookMap } from "@grlt-hub/app-compose"


const hookMap: ComposeHookMap = {
  onStart: (event) => console.log(event.meta),
  onComplete: (event) => console.log(event.meta),
  onTaskFail: (event) => console.log(event.task, event.error),
}
```

## Composer

[Section titled “Composer”](#composer)

The type returned by `compose()`.

Best on desktop edit & run it live Copy link

```ts
import { type Composer, compose } from "@grlt-hub/app-compose"


const app: Composer = compose()
```

## Spot\<T>

[Section titled “Spot\<T>”](#spott)

The type of a value source — created by `literal`, `optional`, `shape`, or exposed as `task.result`, `task.status`, `task.error`, and `tag.value`. `T` is the value type.

## SpotValue\<T>

[Section titled “SpotValue\<T>”](#spotvaluet)

Extracts the value type from a Spot — `T` is the Spot type.

Best on desktop edit & run it live Copy link

```ts
import { type SpotValue, createTask, literal, optional } from "@grlt-hub/app-compose"


const apiUrl = literal("https://api.example.com")
type ApiUrlValue = SpotValue<typeof apiUrl> // => "https://api.example.com"


const port = optional(literal(8080))
type PortValue = SpotValue<typeof port> // => 8080 | undefined


const fetchUser = createTask({
  name: "fetchUser",
  run: { fn: () => ({ id: 1 }) },
})
type FetchUserResult = SpotValue<typeof fetchUser.result> // => { id: string; name: string }
```

## Task\<T>

[Section titled “Task\<T>”](#taskt)

The type of a Task — `T` is its return type. Use it for parameters and variables that hold a Task.

Best on desktop edit & run it live Copy link

```ts
import { type Task, createTask } from "@grlt-hub/app-compose"


const alpha = createTask({
  name: "alpha",
  run: { fn: () => ({ value: "hello" }) },
})


const ref: Task<{ value: string }> = alpha
```

## TaskResult\<T>

[Section titled “TaskResult\<T>”](#taskresultt)

Extracts the return type from a Task — `T` is the Task type.

Best on desktop edit & run it live Copy link

```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)

A Task’s final state: `done`, `fail`, or `skip`.

```ts
type TaskStatus = "done" | "fail" | "skip"
```