AX/T/06 — AX/TUTORIALS
Opublikowano: 12 maj 2026 · 13 min czytania

Jak zbudować bota do wypełniania formularzy webowych

Auto-fill z CRM, multi-step forms, file uploads, hidden validation. Dla rejestracji, applications, zgłoszeń, e-Government.

BeginnerPlaywrightTypeScriptCSV parsingPostgreSQL (status tracking)

Form filling to jeden z najprostszych use-case-ów automation ale jeden z najwięcej powtarzanych. Klasyczne scenariusze:

  • Submitting tej samej oferty do 30 portali (jobs, real estate, marketplaces)
  • Bulk lead intake do CRM (formularz konkurencji)
  • Rejestracja kont na N platformach (warming, testing)
  • Bulk applications (visa forms, gov)
  • Filling structured forms z CSV (np. setki wpisów do bazy gov)

Pokażemy idempotentny pipeline z resumption, error handling, screenshot evidence per submission.

Co potrzebujesz
  • Node.js 20+ lub Python 3.11+
  • Podstawowa znajomość JS/TS lub Python
  • CSV / JSON źródło danych do wypełnienia
  • Datacenter proxy jeśli target nie ma anti-bot
Kroki
  1. 01

    Deklaratywny form descriptor

    Każdy form ma własną strukturę — opisujemy deklaratywnie:

    // forms/lead-intake.ts
    export const leadIntakeForm = {
      url: 'https://competitor.com/contact',
      steps: [
        { field: 'input[name="email"]', value: (data) => data.email, type: 'fill' },
        { field: 'input[name="firstName"]', value: (data) => data.firstName, type: 'fill' },
        { field: 'select[name="industry"]', value: (data) => data.industry, type: 'select' },
        { field: 'textarea[name="message"]', value: (data) => data.message, type: 'fill' },
        { field: 'input[name="consent"]', type: 'check' },
        { field: 'button[type="submit"]', type: 'click' },
      ],
      successIndicator: 'text=Thank you',
      errorIndicators: ['text=Please fill', '.field-error'],
    };
  2. 02

    Form runner — fill, validate, submit

    Generic runner który wykonuje form descriptor:

    async function runForm(formDesc, data) {
      const browser = await chromium.launch({ headless: true });
      const context = await browser.newContext();
      const page = await context.newPage();
      
      try {
        await page.goto(formDesc.url, { waitUntil: 'networkidle' });
        
        for (const step of formDesc.steps) {
          const locator = page.locator(step.field);
          await locator.waitFor({ state: 'visible', timeout: 10000 });
          
          switch (step.type) {
            case 'fill':
              await locator.fill(step.value(data));
              break;
            case 'select':
              await locator.selectOption(step.value(data));
              break;
            case 'check':
              await locator.check();
              break;
            case 'click':
              await locator.click();
              break;
            case 'upload':
              await locator.setInputFiles(step.value(data));
              break;
          }
          
          await sleep(rand(300, 800)); // human-like timing
        }
        
        // Wait for outcome
        const success = page.locator(formDesc.successIndicator);
        const error = page.locator(formDesc.errorIndicators.join(', '));
        await Promise.race([
          success.waitFor({ timeout: 30000 }),
          error.waitFor({ timeout: 30000 }),
        ]);
        
        const isSuccess = await success.isVisible();
        const screenshot = await page.screenshot({ fullPage: true });
        
        return { success: isSuccess, screenshot };
      } finally {
        await browser.close();
      }
    }
  3. 03

    Idempotency i resumption

    1000 forms do submit. Po 300 process pada. Bez idempotency: zaczynasz od zera, 300 duplikatów. Z idempotency: kontynuujesz od 301.

    CREATE TABLE form_submissions (
      id BIGSERIAL PRIMARY KEY,
      external_id TEXT UNIQUE NOT NULL,  -- unique per data row
      form_id TEXT NOT NULL,
      status TEXT NOT NULL CHECK (status IN ('pending', 'submitted', 'failed')),
      data JSONB NOT NULL,
      screenshot_path TEXT,
      attempts INT NOT NULL DEFAULT 0,
      last_error TEXT,
      submitted_at TIMESTAMPTZ
    );
    
    CREATE INDEX idx_form_status ON form_submissions (form_id, status);
    async function processBatch(formDesc, dataRows) {
      for (const data of dataRows) {
        const externalId = generateId(data); // np. email + timestamp
        
        // Skip if already submitted
        const existing = await db.query(
          'SELECT status FROM form_submissions WHERE external_id=$1',
          [externalId]
        );
        if (existing.rows.length && existing.rows[0].status === 'submitted') {
          continue; // idempotent
        }
        
        // Insert pending
        await db.query(`
          INSERT INTO form_submissions (external_id, form_id, status, data)
          VALUES ($1, $2, 'pending', $3)
          ON CONFLICT (external_id) DO UPDATE SET attempts = form_submissions.attempts + 1
        `, [externalId, formDesc.id, data]);
        
        try {
          const result = await runForm(formDesc, data);
          await db.query(`
            UPDATE form_submissions SET status=$1, screenshot_path=$2, submitted_at=NOW()
            WHERE external_id=$3
          `, [result.success ? 'submitted' : 'failed', result.screenshotPath, externalId]);
        } catch (err) {
          await db.query(`
            UPDATE form_submissions SET status='failed', last_error=$1
            WHERE external_id=$2
          `, [err.message, externalId]);
        }
      }
    }
  4. 04

    Multi-step forms i file uploads

    Wizard forms (step 1 → step 2 → review → submit). Każdy step ma "Next" button. File uploads przez setInputFiles:

    // Multi-step descriptor
    const visaApplicationForm = {
      url: 'https://gov.example.com/visa-application',
      steps: [
        // Step 1: Personal info
        { field: 'input[name="firstName"]', type: 'fill', value: d => d.firstName },
        { field: 'input[name="lastName"]', type: 'fill', value: d => d.lastName },
        { field: 'input[name="dateOfBirth"]', type: 'fill', value: d => d.dob },
        { field: 'button:has-text("Next")', type: 'click', waitAfter: 2000 },
        
        // Step 2: Documents (file upload)
        { field: 'input[name="passport-scan"]', type: 'upload', value: d => d.passportPath },
        { field: 'input[name="photo"]', type: 'upload', value: d => d.photoPath },
        { field: 'button:has-text("Next")', type: 'click', waitAfter: 2000 },
        
        // Step 3: Review and submit
        { field: 'input[name="confirm"]', type: 'check' },
        { field: 'button[type="submit"]', type: 'click' },
      ],
      successIndicator: 'text=Application submitted',
      errorIndicators: ['.error', 'text=Invalid'],
    };
Ile to kosztuje uruchomić

Run cost dla 1000 form submissions/mies:

  • VPS (Hetzner CX22): €5/mies
  • Storage (screenshots, DB ~5GB/mies): $5-10/mies
  • Proxy (datacenter, jeśli potrzeba — większość form-ów nie ma anti-bot): $0-15/mies

Total: ~$10-25/mies. Najtańszy use-case w automation.

Częste pułapki
  • Brak waitFor po każdym step — bot wpisuje za szybko, form myśli że nie wpisałeś (debounced validation). Sleep 300-800ms między fields.
  • Dynamic selectors — gdy form używa input_123 generated IDs, selektor padnie po deploy. Używaj aria-label, name, lub placeholder.
  • Brak error handling per step — jeśli validation odrzuca field 3 z 8, bot rusza dalej i submit pada. Sprawdzaj error indicators po każdym step.
  • Cookie/consent popups — przed first form interaction. Auto-dismiss przez "[data-action='accept']" click.
  • Brak proof of submission — bez screenshotu i confirmation ID nie wiesz czy się powiodło. Zawsze save evidence.
Build yourself czy zlecić?

Form automation = entry-level. Większość form-ów (np. publicznych portali, gov, prostych B2B) ogarniesz w 1-2 dniach.

Kiedy się robi trudno: forms z heavy JavaScript validation, multi-step z dynamic field generation (np. visa applications, KYC flows), reCAPTCHA, signing przez biometrię/SMS. Tam my przejmujemy.

Najczęściej zadawane pytania
Czy mogę submit-ować 1000 formularzy jednocześnie?
Nie. Concurrent submissions wyglądają jak DDoS i większość form-ów ma rate limiting. Standard: sequential z 2-8s delay between submissions, max 3 concurrent dla form-ów z high traffic. 1000 formularzy submit-uje się w 2-5h, nie 5 min.
Co z reCAPTCHA / Cloudflare Turnstile?
Trzy opcje: 1) paid solver (2Captcha, Anti-Captcha ~$2/1000 captcha), 2) Capsolver (AI-based, lepsza accuracy), 3) human escape — flow zatrzymuje się, wysyła screenshot na Slack, człowiek rozwiązuje, bot kontynuuje. Najlepiej: AVOID form-ów z captcha jeśli można.
Czy submission gov forms (visa, KYC) jest legalne?
Jurisdiction-specific. UK Home Office wprost zabrania automation visa forms (ToS violation). US USCIS toleruje gdy applicant jest realny human autoryzujący każdy submit. Polska KSeF/CEIDG mają oficjalne API — uzywaj API zamiast scrape. Konsultuj z prawnikiem.
Co jeśli form ma dynamic field IDs które się zmieniają?
Nie używaj ID selectors. Lepiej: aria-label, name attribute, placeholder text, lub text-based selectors ("button:has-text(Next)"). Resilient selektory wytrzymują frontend redeployы; ID-based padają przy każdym build.