Skip to content

Passing Data — Advanced

In 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.

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.

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:

// 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))] })
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()

You can also provide data directly in the context — either a static value or another task’s result:

// 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.

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