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
.hirafiles
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.jsonCreate 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.tsEach .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
- New version = new folder — never modify old versions, create
v1.1.0/next tov1.0.0/ - Share code via
shared/— common helpers, login logic, etc. - Each version has its own config — add new inputs/outputs without breaking old versions
- Independent builds — each version builds to a separate
.hirafile - Test locally first — use
main.dev.tswithts-nodebefore building
Last updated on