Building a Real-World Task

This content gets you from hello world to some learn the task components to build a couple of real world tasks

Published: 4/18/2025

In the previous tutorial, we covered how to run a simple "Hello World" task. Now, let's dive deeper by exploring how to build more practical tasks that interact with external services. In this case we will use the yahoo finance API since the info changes price often and doesnt require a API token to be created for the example.

First lets create a new task with

forge task:create stocks:getPrice

This will make the following file

// TASK: getPrice
// Run this task with:
// forge task:run stocks:getPrice

import { createTask } from '@forgehive/task'
import { Schema } from '@forgehive/schema'

const description = 'Add task description here'

const schema = new Schema({
  // Add your schema definitions here
  // example: myParam: Schema.string()
})

const boundaries = {
  // Add your boundary functions here
  // example: readFile: async (path: string) => fs.readFile(path, 'utf-8')
}

export const getPrice = createTask(
  schema,
  boundaries,
  async function (argv, boundaries) {
    console.log('input:', argv)
    console.log('boundaries:', boundaries)
    // Your task implementation goes here
    const status = { status: 'Ok' }

    return status
  }
)

getPrice.setDescription(description)

When you create a task, Forge&Hive automatically updates the forge.json file to register the task with its path and handler. This registration is essential for the CLI to locate and run your task:

{
  "tasks": {
    "stocks:getPrice": {
      "path": "src/tasks/stocks/getPrice.ts",
      "handler": "getPrice"
    }
  }
}

If at some point you move a task to another folder, rename it or change the handler, update this record so you can keep running it from the CLI.

Understanding Task Components

Every Forge&Hive task consists of three main parts: the Schema for input validation, Boundaries for external dependencies, and the Handler containing the business logic. Let's modify each component to create a real stock price checker.

1. Updating the Description

First, let's update the task description to clearly explain what our task does:

const description = 'Get the price of a stock by ticker in real time'

The description helps document the task's purpose - it's a great way to know what to expect when opening the file since it's positioned at the top. Think of it as both documentation and metadata that helps maintain the project as it grows, also will be relevant when converting tasks to chatbot tools in future projects.

2. Defining the Schema

The schema specifies the structure and validation rules for our task's input. For our stock price task, we need a ticker symbol:

const schema = new Schema({
  ticker: Schema.string()
})

This schema:

  • Defines a single parameter called ticker
  • Ensures it's a string (e.g., "AAPL" for Apple Inc.)
  • Makes the parameter required (the task won't run without it)

If we wanted to make the ticker optional or add more parameters, we could do:

const schema = new Schema({
  ticker: Schema.string(),
  currency: Schema.string().optional(),
  detailed: Schema.boolean().optional()
})

3. Implementing Boundaries

Boundaries represent interfaces to external dependencies. Boundaries should be added when the call to the function is not deterministic. Tasks will help you track the arguments that were used and the response of the call in the logs which makes it really easy to debug.

The combination of input and boundaries will allow us to have a good fingerprint of the function execution.

For our stock price task, we need to fetch data from Yahoo Finance:

const boundaries = {
  fetchYahooFinance: async (ticker: string) => {
    const url = `https://query1.finance.yahoo.com/v8/finance/chart/${ticker}`
    const response = await fetch(url)
    const data = await response.json()

    const quote = data.chart.result[0].meta
    const price = quote.regularMarketPrice
    const currency = quote.currency

    return {
      price,
      currency
    }
  }
}

This boundary:

  • Isolates the external API call
  • Returns a structured result with price and currency
  • Can be easily mocked for testing

4. Writing the Task Handler

The task handler contains our business logic:

async function ({ ticker }, { fetchYahooFinance }) {
  const { price, currency } = await fetchYahooFinance(ticker)

  console.log('================================');
  console.log('Price:', price);
  console.log('================================');

  return {
    ticker,
    price,
    currency
  }
}

Note how we:

  • Use destructuring to access the input parameter (ticker)
  • Use destructuring to access the boundary function (fetchYahooFinance)
  • Log information for debugging
  • Return a structured result with all relevant information

The Complete Task

Here's our complete stock price task:

// TASK: getPrice
// Run this task with:
// forge task:run stocks:getPrice --ticker AAPL

import { createTask } from '@forgehive/task'
import { Schema } from '@forgehive/schema'

const description = 'Get the price of a stock by ticker in real time'

const schema = new Schema({
  ticker: Schema.string()
})

const boundaries = {
  fetchYahooFinance: async (ticker: string) => {
    const url = `https://query1.finance.yahoo.com/v8/finance/chart/${ticker}`
    const response = await fetch(url)
    const data = await response.json()

    const quote = data.chart.result[0].meta
    const price = quote.regularMarketPrice
    const currency = quote.currency

    return {
      price,
      currency
    }
  }
}

export const getPrice = createTask(
  schema,
  boundaries,
  async function ({ ticker }, { fetchYahooFinance }) {
    const { price, currency } = await fetchYahooFinance(ticker)

    console.log('Price:', price);

    return {
      ticker,
      price,
      currency
    }
  }
)

getPrice.setDescription(description)

Running the Stock Price Task

To run our task:

forge task:run stocks:getPrice --ticker AAPL

The output will look something like:

========================================
Running: task:run stocks:getPrice { ticker: 'AAPL' }
========================================
Price: 196.98
===============================================
Outcome {
  outcome: 'Success',
  taskName: 'task:run',
  result: { ticker: 'AAPL', price: 196.98, currency: 'USD' }
}
===============================================

You can also programmatically import and run the task in your code:

import { getPrice } from './tasks/stocks/getPrice';

// Run the task programmatically
const result = await getPrice.run({ ticker: 'AAPL' });
console.log('Stock price:', result.price);

This makes tasks highly reusable - they can be run both from the CLI during development and imported directly in your application code.

Examining the Task Execution Log

When you run a task, Forge creates a detailed execution log in the logs directory. Looking at the log file (named something like logs/stocks:getPrice.log), you'll see:

{
  "name": "stocks:getPrice",
  "type": "success",
  "input": {
    "ticker": "AAPL"
  },
  "output": {
    "ticker": "AAPL",
    "price": 196.98,
    "currency": "USD"
  },
  "boundaries": {
    "fetchYahooFinance": [
      {
        "input": ["AAPL"],
        "output": {
          "price": 196.98,
          "currency": "USD"
        }
      }
    ]
  }
}

This log provides complete visibility into:

  • The input parameters
  • All boundary calls (with inputs, outputs, and durations)
  • The task result
  • Execution timing information

Benefits of the Task and Boundaries Pattern

By structuring your code as tasks with boundaries, you gain:

  1. Type Safety - Input validation using schemas ensures your task only runs with valid data.

  2. Testability - Boundaries can be easily mocked for unit testing without hitting real external services.

  3. Separation of Concerns - Clear distinction between business logic (in the handler) and external interactions (in boundaries).

  4. Debuggability - Complete execution logs make it easy to understand what happened during task execution.

  5. Reusability - Tasks can be composed and reused across your application.

  6. Error Handling - Schema validation catches input errors early, and boundary isolation makes external errors more manageable.

  7. Documentation - The schema serves as self-documenting API for task parameters.

In the next tutorial, we'll explore advanced task features including execution modes, task composition, and integrating tasks into larger applications.