Skip to Content
ExamplesMulti-Flow Project

Multi-Flow Project

Build a project with multiple flows and versions — a real-world setup for managing several automation scripts in one codebase.

What you’ll learn:

  • Organizing multiple flows in one project
  • Version management per flow
  • Sharing code between flows
  • Switching between GPM and Hidemium providers
  • Building individual flows as .hira files

Project Structure

my-automation-project/ ├── src/ │ ├── flows/ │ │ ├── shared/ ← Shared utilities │ │ │ └── helpers.ts │ │ ├── login-flow/ ← Flow 1 │ │ │ ├── v1.0.0/ │ │ │ │ ├── index.ts │ │ │ │ ├── logic.ts │ │ │ │ └── profiles.xlsx │ │ │ └── v1.1.0/ ← New version │ │ │ ├── index.ts │ │ │ ├── logic.ts │ │ │ └── profiles.xlsx │ │ ├── scraper-flow/ ← Flow 2 │ │ │ └── v1.0.0/ │ │ │ ├── index.ts │ │ │ └── logic.ts │ │ └── register-flow/ ← Flow 3 │ │ └── v1.0.0/ │ │ ├── index.ts │ │ └── logic.ts │ └── main.dev.ts ← Dev runner ├── package.json └── tsconfig.json

Create a shared helper

src/flows/shared/helpers.ts — utility functions used across flows:

import { BrowserUtils, IScriptContext, IFlowConfig } from '@hira-core/sdk' /** * Common login helper — reusable across flows */ export async function performLogin( browser: BrowserUtils, username: string, password: string, ): Promise<boolean> { await browser.type('#username', username) await browser.type('#password', password) await browser.click('#login-btn') await browser.waitForNavigation() return await browser.exists('#dashboard', 10000) } /** * Common cookie dismiss helper */ export async function dismissCookieBanner(browser: BrowserUtils) { const hasBanner = await browser.exists('.cookie-banner', 3000) if (hasBanner) { await browser.click('.cookie-accept') await browser.sleep(500) } }

Create Flow 1 — Login Flow (v1.0.0)

src/flows/login-flow/v1.0.0/logic.ts:

import { AntidetectBaseFlow, AntidetectProvider, BrowserUtils, FlowLogger, IScriptContext, defineFlowConfig, } from '@hira-core/sdk' import { performLogin, dismissCookieBanner } from '../../shared/helpers' const config = defineFlowConfig({ globalInput: [ { key: 'targetUrl', label: 'URL', type: 'text', required: true }, ], profileInput: [ { key: 'username', label: 'Username', type: 'text', required: true }, { key: 'password', label: 'Password', type: 'text', required: true }, ], output: [ { index: 0, key: 'loginStatus', label: 'Status' }, { index: 1, key: 'pageTitle', label: 'Title' }, ], }) type Config = typeof config export class LoginFlow extends AntidetectBaseFlow<Config> { // Accept provider as parameter → switch between GPM/Hidemium constructor(provider: AntidetectProvider = AntidetectProvider.GPM) { super(provider, new FlowLogger('LoginFlow'), config) } async script(context: IScriptContext<Config>) { const browser = new BrowserUtils(context) const { globalInput, profileInput } = context await browser.logProfileInput() // Navigate await browser.goto(globalInput.targetUrl) await dismissCookieBanner(browser) // shared helper // Login using shared helper const ok = await performLogin( browser, profileInput.username, profileInput.password, ) if (ok) { const title = await context.page.title() await browser.writeOutput('loginStatus', 'OK') await browser.writeOutput('pageTitle', title) } else { await browser.writeOutput('loginStatus', 'FAILED') await browser.screenshot('login-failed.png') } } }

src/flows/login-flow/v1.0.0/index.ts:

export { LoginFlow as default } from './logic'

Create Flow 1 — New Version (v1.1.0)

When you need to update a flow, create a new version folder:

src/flows/login-flow/v1.1.0/logic.ts:

import { AntidetectBaseFlow, AntidetectProvider, BrowserUtils, FlowLogger, IScriptContext, defineFlowConfig, } from '@hira-core/sdk' import { performLogin, dismissCookieBanner } from '../../shared/helpers' const config = defineFlowConfig({ globalInput: [ { key: 'targetUrl', label: 'URL', type: 'text', required: true }, { key: 'maxRetries', label: 'Max Retries', type: 'number', defaultValue: 3 }, ], profileInput: [ { key: 'username', label: 'Username', type: 'text', required: true }, { key: 'password', label: 'Password', type: 'text', required: true }, { key: 'twofa', label: '2FA Code', type: 'text' }, // ← new field in v1.1.0 ], output: [ { index: 0, key: 'loginStatus', label: 'Status' }, { index: 1, key: 'pageTitle', label: 'Title' }, { index: 2, key: 'cookies', label: 'Cookies' }, // ← new output in v1.1.0 ], }) type Config = typeof config export class LoginFlowV2 extends AntidetectBaseFlow<Config> { constructor(provider: AntidetectProvider = AntidetectProvider.GPM) { super(provider, new FlowLogger('LoginFlow-v1.1.0'), config) } async script(context: IScriptContext<Config>) { const browser = new BrowserUtils(context) const { globalInput, profileInput } = context await browser.goto(globalInput.targetUrl) await dismissCookieBanner(browser) const ok = await performLogin( browser, profileInput.username, profileInput.password, ) if (!ok) { await browser.writeOutput('loginStatus', 'FAILED') return } // v1.1.0: Handle 2FA if provided if (profileInput.twofa) { const has2fa = await browser.exists('#twofa-input', 5000) if (has2fa) { await browser.type('#twofa-input', profileInput.twofa) await browser.click('#verify-btn') await browser.waitForNavigation() } } const title = await context.page.title() await browser.writeOutput('loginStatus', 'OK') await browser.writeOutput('pageTitle', title) // v1.1.0: Save cookies for later reuse const cookies = await context.page.cookies() await browser.writeOutput('cookies', JSON.stringify(cookies.slice(0, 5))) } }

src/flows/login-flow/v1.1.0/index.ts:

export { LoginFlowV2 as default } from './logic'

Create Flow 2 — Scraper Flow

src/flows/scraper-flow/v1.0.0/logic.ts:

import { AntidetectBaseFlow, AntidetectProvider, BrowserUtils, FlowLogger, IScriptContext, defineFlowConfig, } from '@hira-core/sdk' const config = defineFlowConfig({ globalInput: [ { key: 'targetUrl', label: 'Target URL', type: 'text', required: true }, { key: 'maxPages', label: 'Max Pages', type: 'number', defaultValue: 5 }, ], profileInput: [], output: [ { index: 0, key: 'pagesScraped', label: 'Pages Scraped' }, { index: 1, key: 'dataCollected', label: 'Data Count' }, ], }) type Config = typeof config export class ScraperFlow extends AntidetectBaseFlow<Config> { // Use Hidemium instead of GPM constructor(provider: AntidetectProvider = AntidetectProvider.HIDEMIUM) { super(provider, new FlowLogger('ScraperFlow'), config) } async script(context: IScriptContext<Config>) { const browser = new BrowserUtils(context) const { globalInput } = context let pagesScraped = 0 await browser.goto(globalInput.targetUrl) for (let i = 0; i < globalInput.maxPages; i++) { const title = await context.page.title() context.logger.info(`📄 Page ${i + 1}: ${title}`) pagesScraped++ const hasNext = await browser.exists('a.next-page', 3000) if (!hasNext) break await browser.click('a.next-page') await browser.waitForNavigation() await browser.sleep(1000) } await browser.writeOutput('pagesScraped', pagesScraped) await browser.writeOutput('dataCollected', pagesScraped * 10) } }

src/flows/scraper-flow/v1.0.0/index.ts:

export { ScraperFlow as default } from './logic'

Create the dev runner

src/main.dev.ts — switch between flows easily:

import { AntidetectProvider } from '@hira-core/sdk' // ── Pick which flow to run ── // import Flow from './flows/login-flow/v1.0.0' // Login v1.0.0 // import Flow from './flows/login-flow/v1.1.0' // Login v1.1.0 import Flow from './flows/scraper-flow/v1.0.0' // Scraper async function bootstrap() { // Switch provider here: // AntidetectProvider.GPM → GPM Login (default port 19995) // AntidetectProvider.HIDEMIUM → Hidemium (default port 2222) const flow = new Flow(AntidetectProvider.GPM) // Optional: setup Excel storage const storage = flow.createExcelStorage({ inputFile: { filePath: './profiles.xlsx', profileNameColumn: 'A', columns: { 'B': 'username', 'C': 'password' }, }, outputFile: { filePath: './profiles.xlsx', profileNameColumn: 'A', columns: { 'D': 'loginStatus', 'E': 'pageTitle' }, }, }, __dirname) const profileSettings = await storage.parseProfileSettings() const params = flow.createRunParams({ antidetect: { profileSettings }, execution: { concurrency: 3, maxRetries: 1, keepOpenOnError: true, }, globalInput: { targetUrl: 'https://example.com', maxPages: 5, }, window: { width: 1280, height: 720, scale: 0.6 }, }) console.log('▶️ Running...') await flow.run(params) console.log('✅ Done!') } bootstrap()

Build individual flows

Each version can be built as a separate .hira file:

# Build login flow v1.0.0 hira-cli build --entry src/flows/login-flow/v1.0.0/index.ts # Build login flow v1.1.0 (new version) hira-cli build --entry src/flows/login-flow/v1.1.0/index.ts # Build scraper flow hira-cli build --entry src/flows/scraper-flow/v1.0.0/index.ts

Each .hira file can be imported into the Hira desktop app independently.


AntidetectProvider Reference

Switch anti-detect browser by changing the constructor parameter:

enum AntidetectProvider { GPM = 'gpm', // GPM Login — default API: http://127.0.0.1:19995 HIDEMIUM = 'hidemium', // Hidemium — default API: http://127.0.0.1:2222 GENLOGIN = 'genlogin', // GenLogin ADSPOWER = 'adspower', // AdsPower }
// Use GPM const flow = new LoginFlow(AntidetectProvider.GPM) // Use Hidemium const flow = new LoginFlow(AntidetectProvider.HIDEMIUM)

Version Management Tips

  1. New version = new folder — never modify old versions, create v1.1.0/ next to v1.0.0/
  2. Share code via shared/ — common helpers, login logic, etc.
  3. Each version has its own config — add new inputs/outputs without breaking old versions
  4. Independent builds — each version builds to a separate .hira file
  5. Test locally first — use main.dev.ts with ts-node before building
Last updated on