Skip to Content
ExamplesBasic Flow (Login + Output)

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 BrowserUtils for browser interaction
  • Building and deploying as .hira file

Create the project folder

mkdir my-login-flow cd my-login-flow

Create 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 install

Define 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 const on the arrays for full TypeScript autocomplete on context.globalInput, context.profileInput, and writeOutput().

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

Build and deploy

Build the .hira flow bundle:

pnpm run build

This creates a dist/my-login-flow.hira file that you can import into the Hira desktop app.

To import:

  1. Open Hira desktop
  2. Go to Library
  3. Click β€œImport Flow”
  4. Select the .hira file

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

  1. defineFlowConfig() β€” declare inputs/outputs upfront for type safety
  2. script(context) β€” your main logic, called once per profile
  3. BrowserUtils β€” wraps Puppeteer with logging, abort handling, and error safety
  4. writeOutput() β€” save results for each profile, persisted across runs
  5. output.* β€” read previous output values to skip already-completed profiles
  6. Return values β€” click(), type(), goto() return boolean, check them!
Last updated on