Basic Flow β Login + Output
Build a complete automation flow that navigates to a website, logs in with per-profile credentials, reads page data, and writes output results.
What youβll learn:
- Setting up the project
- Defining config with input/output schema
- Writing the
script()automation logic - Using
BrowserUtilsfor browser interaction - Building and deploying as
.hirafile
Create the project folder
mkdir my-login-flow
cd my-login-flowCreate package.json:
{
"name": "my-login-flow",
"version": "1.0.0",
"main": "src/index.ts",
"scripts": {
"build": "hira-cli build"
},
"dependencies": {
"@hira-core/sdk": "^1.0.6"
},
"devDependencies": {
"@hira-core/cli": "^1.0.0",
"typescript": "^5.0.0"
}
}Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"lib": ["ES2019"],
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true
},
"include": ["src"]
}Install dependencies:
pnpm installDefine the flow config
Create src/config.ts β this declares what inputs and outputs your flow has:
import { defineFlowConfig } from '@hira-core/sdk'
export const config = defineFlowConfig({
// Global inputs β shared across all profiles
globalInput: [
{
key: 'targetUrl',
label: 'Target URL',
type: 'text',
required: true,
},
{
key: 'delay',
label: 'Delay between actions (ms)',
type: 'number',
defaultValue: 3000,
},
],
// Per-profile inputs β each profile has its own values
profileInput: [
{
key: 'username',
label: 'Username',
type: 'text',
required: true,
},
{
key: 'password',
label: 'Password',
type: 'text',
required: true,
},
],
// Output β results for each profile
output: [
{
index: 0,
key: 'loginStatus',
label: 'Login Status',
},
{
index: 1,
key: 'pageTitle',
label: 'Page Title',
},
],
})[!TIP] Use
as conston the arrays for full TypeScript autocomplete oncontext.globalInput,context.profileInput, andwriteOutput().
Write the script logic
Create src/flow.ts β this is where your automation runs:
import {
AntidetectBaseFlow,
AntidetectProvider,
BrowserUtils,
FlowLogger,
IScriptContext,
} from '@hira-core/sdk'
import { config } from './config'
type FlowConfig = typeof config
export class LoginFlow extends AntidetectBaseFlow<FlowConfig> {
constructor() {
super(
AntidetectProvider.GPM, // anti-detect browser provider
new FlowLogger('LoginFlow'), // logger
config, // type-safe config
)
}
async script(context: IScriptContext<FlowConfig>) {
const { page, logger, globalInput, profileInput, output } = context
const browser = new BrowserUtils(context)
// ββββββββββββββββββββββββββββββββββββββ
// 1. Log current profile data
// ββββββββββββββββββββββββββββββββββββββ
await browser.logProfileInput()
// Log:
// π Profile Input:
// {
// username: "admin",
// password: "secret123"
// }
await browser.logProfileOutput()
// Log: π Profile Output: { loginStatus: null, pageTitle: null }
// ββββββββββββββββββββββββββββββββββββββ
// 2. Check if already logged in (from previous run)
// ββββββββββββββββββββββββββββββββββββββ
if (output.loginStatus === 'OK') {
logger.info('Already logged in from previous run, skipping')
return
}
// ββββββββββββββββββββββββββββββββββββββ
// 3. Navigate to target URL
// ββββββββββββββββββββββββββββββββββββββ
const url = globalInput.targetUrl
await browser.goto(url)
// Log: π Navigate β https://example.com
// ββββββββββββββββββββββββββββββββββββββ
// 4. Login with profile credentials
// ββββββββββββββββββββββββββββββββββββββ
await browser.type('#username', profileInput.username)
// Log: β¨οΈ Type "admin" β #username
await browser.type('#password', profileInput.password)
// Log: β¨οΈ Type "secret123" β #password
await browser.click('#login-btn')
// Log: π±οΈ Click: #login-btn
// ββββββββββββββββββββββββββββββββββββββ
// 5. Wait for page to load & verify result
// ββββββββββββββββββββββββββββββββββββββ
await browser.waitForNavigation()
// Log: π Waiting for navigation...
const isLoggedIn = await browser.exists('#dashboard', 10000)
if (!isLoggedIn) {
await browser.writeOutput('loginStatus', 'FAILED')
// Log: π€ Write Output [loginStatus] = FAILED
await browser.screenshot('login-failed.png')
// Log: πΈ Screenshot: login-failed.png
return
}
// ββββββββββββββββββββββββββββββββββββββ
// 6. Read page data
// ββββββββββββββββββββββββββββββββββββββ
const title = await page.title()
logger.success(`π Page Title: ${title}`)
// ββββββββββββββββββββββββββββββββββββββ
// 7. Write output results
// ββββββββββββββββββββββββββββββββββββββ
await browser.writeOutput('pageTitle', title)
// Log: π€ Write Output [pageTitle] = Dashboard
await browser.writeOutput('loginStatus', 'OK')
// Log: π€ Write Output [loginStatus] = OK
// ββββββββββββββββββββββββββββββββββββββ
// 8. Verify output was saved (real-time update)
// ββββββββββββββββββββββββββββββββββββββ
logger.info(`Status after write: ${output.loginStatus}`) // β "OK"
await browser.logProfileOutput()
// Log:
// π Profile Output:
// {
// loginStatus: "OK",
// pageTitle: "Dashboard"
// }
}
}Create the entry point
Create src/index.ts β the file that exports your flow class:
export { LoginFlow as default } from './flow'[!IMPORTANT] The entry point must use
export default. The SDK uses this to find and instantiate your flow.
Set up the project structure
Your project should now look like this:
my-login-flow/
βββ src/
β βββ config.ts β defineFlowConfig() β input/output schema
β βββ flow.ts β LoginFlow class β script() logic
β βββ index.ts β Default export
βββ package.json
βββ tsconfig.jsonBuild and deploy
Build the .hira flow bundle:
pnpm run buildThis creates a dist/my-login-flow.hira file that you can import into the Hira desktop app.
To import:
- Open Hira desktop
- Go to Library
- Click βImport Flowβ
- Select the
.hirafile
Full script() flow diagram
script(context) called
β
ββ logProfileInput() β see all profile data
ββ logProfileOutput() β see previous results
β
ββ Check output.loginStatus === 'OK'?
β ββ YES β return early (skip)
β
ββ goto(targetUrl) β navigate to site
ββ type('#username') β enter username
ββ type('#password') β enter password
ββ click('#login-btn') β submit form
β
ββ waitForNavigation() β wait for page load
ββ exists('#dashboard') β verify login success
β ββ NO β writeOutput('loginStatus', 'FAILED') + screenshot β return
β
ββ page.title() β read page data
ββ writeOutput('pageTitle', title)
ββ writeOutput('loginStatus', 'OK')Key Takeaways
defineFlowConfig()β declare inputs/outputs upfront for type safetyscript(context)β your main logic, called once per profileBrowserUtilsβ wraps Puppeteer with logging, abort handling, and error safetywriteOutput()β save results for each profile, persisted across runsoutput.*β read previous output values to skip already-completed profiles- Return values β
click(),type(),goto()returnboolean, check them!