> ## Documentation Index
> Fetch the complete documentation index at: https://skyvern.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Healthcare Portal Data Extraction

> Extract patient demographics and billing data from OpenEMR using Skyvern agents with residential proxies, browser profiles, and loop blocks for multi-page pagination.

This cookbook extracts two datasets from [OpenEMR](https://www.open-emr.org/), an open-source EHR, using the public demo at `https://demo.openemr.io/openemr/index.php`:

1. **Patient demographics** from Patient/Client > Finder
2. **Encounter billing data** from Reports > Visits > Superbill

**Demo credentials:** `admin` / `pass` (resets daily at 8:00 AM UTC)

***

## Prerequisites

* A [Skyvern Cloud](https://app.skyvern.com) account or [self-hosted](/developers/self-hosted/overview) deployment
* The Skyvern SDK (for API usage)

<CodeGroup>
  ```bash Python theme={null}
  pip install skyvern
  ```

  ```bash TypeScript theme={null}
  npm install @skyvern/client
  ```
</CodeGroup>

***

## Why a single task isn't enough

A basic task pointed at OpenEMR with a vague prompt will partially work, but hits four problems in production:

<CodeGroup>
  ```python Python theme={null}
  result = await client.run_task(
      url="https://demo.openemr.io/openemr/index.php",
      prompt="Log in and extract the patient list",
  )
  ```

  ```typescript TypeScript theme={null}
  const result = await client.runTask({
    body: {
      url: "https://demo.openemr.io/openemr/index.php",
      prompt: "Log in and extract the patient list",
    },
  });
  ```
</CodeGroup>

| Problem          | Impact                                                             |
| ---------------- | ------------------------------------------------------------------ |
| No proxy         | Production EHR portals sit behind WAFs that block datacenter IPs   |
| Login every run  | Wastes steps, fragile with session complexity                      |
| Vague navigation | OpenEMR uses iframes and dynamic menus, so it needs explicit goals |
| No pagination    | Only gets page 1 of multi-page results                             |

The sections below solve each one.

***

## Residential proxies

Route the browser through a residential IP to bypass WAF/bot detection. The demo works without one, but production portals require it.

<Tabs>
  <Tab title="Cloud UI">
    In the run panel, expand **Advanced Settings** and set **Proxy Location** to a country (e.g., **United States**).
  </Tab>

  <Tab title="API / SDK">
    <CodeGroup>
      ```python Python theme={null}
      result = await client.run_task(
          url="https://demo.openemr.io/openemr/index.php",
          prompt="Log in with username 'admin' and password 'pass', confirm the Calendar page loads",
          proxy_location="RESIDENTIAL",
      )
      ```

      ```typescript TypeScript theme={null}
      const result = await client.runTask({
        body: {
          url: "https://demo.openemr.io/openemr/index.php",
          prompt: "Log in with username 'admin' and password 'pass', confirm the Calendar page loads",
          proxy_location: "RESIDENTIAL",
        },
      });
      ```
    </CodeGroup>
  </Tab>
</Tabs>

See [Proxy & Geolocation](/developers/going-to-production/proxy-geolocation) for all available locations.

***

## Browser profiles

Log in once, save the browser state as a profile, and skip login on future runs.

<Tabs>
  <Tab title="Cloud UI">
    <Steps>
      <Step title="Create a login agent">
        Go to **Agents** in the sidebar and create a new agent. Add a **Navigation** block with URL `https://demo.openemr.io/openemr/index.php` and goal: "Log in with username 'admin' and password 'pass'. Confirm the Calendar page loads."

        On the **Start** node, expand the settings and enable **Save & Reuse Session**. Set **Proxy Location** to a country (e.g., **United States**).

        <Frame>
          <video controls muted playsInline className="w-full aspect-video rounded-xl" src="https://mintcdn.com/skyvern/mJBhj6Qa9IBxG0NG/images/workflow-start.mp4?fit=max&auto=format&n=mJBhj6Qa9IBxG0NG&q=85&s=3d57ece7ff94254eae70e2732cb9de71" data-path="images/workflow-start.mp4" />
        </Frame>
      </Step>

      <Step title="Run the agent">
        Run the agent and wait for it to complete.
      </Step>

      <Step title="Create a profile via API">
        Browser profile creation is done via the API. Use the `create_browser_profile` call from the API/SDK tab with the completed agent run ID. Name it `openemr-demo-admin`.
      </Step>
    </Steps>
  </Tab>

  <Tab title="API / SDK">
    <CodeGroup>
      ```python Python theme={null}
      import asyncio
      from skyvern import Skyvern

      async def main():
          client = Skyvern(api_key="YOUR_API_KEY")

          # 1. Create workflow that saves browser state
          workflow = await client.create_workflow(
              json_definition={
                  "title": "OpenEMR Login",
                  "persist_browser_session": True,
                  "workflow_definition": {
                      "parameters": [],
                      "blocks": [
                          {
                              "block_type": "navigation",
                              "label": "login",
                              "url": "https://demo.openemr.io/openemr/index.php",
                              "navigation_goal": (
                                  "Log in with username 'admin' and password 'pass'. "
                                  "Confirm the Calendar page or main dashboard loads."
                              ),
                          }
                      ],
                  },
              }
          )

          # 2. Run with residential proxy
          run = await client.run_workflow(
              workflow_id=workflow.workflow_permanent_id,
              proxy_location="RESIDENTIAL",
              wait_for_completion=True,
          )
          print(f"Login: {run.status}")  # completed

          # 3. Save profile (retry while session archives)
          profile = None
          for attempt in range(10):
              try:
                  profile = await client.create_browser_profile(
                      name="openemr-demo-admin",
                      workflow_run_id=run.run_id,
                  )
                  break
              except Exception as e:
                  if "persisted" in str(e).lower() and attempt < 9:
                      await asyncio.sleep(2)
                      continue
                  raise

          print(f"Profile: {profile.browser_profile_id}")

      asyncio.run(main())
      ```

      ```typescript TypeScript theme={null}
      import { Skyvern } from "@skyvern/client";

      async function main() {
        const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });

        // 1. Create workflow that saves browser state
        const workflow = await client.createWorkflow({
          body: {
            json_definition: {
              title: "OpenEMR Login",
              persist_browser_session: true,
              workflow_definition: {
                parameters: [],
                blocks: [
                  {
                    block_type: "navigation",
                    label: "login",
                    url: "https://demo.openemr.io/openemr/index.php",
                    navigation_goal:
                      "Log in with username 'admin' and password 'pass'. " +
                      "Confirm the Calendar page or main dashboard loads.",
                  },
                ],
              },
            },
          },
        });

        // 2. Run with residential proxy
        const run = await client.runWorkflow({
          body: {
            workflow_id: workflow.workflow_permanent_id,
            proxy_location: "RESIDENTIAL",
          },
          waitForCompletion: true,
        });
        console.log(`Login: ${run.status}`);

        // 3. Save profile (retry while session archives)
        let profile;
        for (let attempt = 0; attempt < 10; attempt++) {
          try {
            profile = await client.createBrowserProfile({
              name: "openemr-demo-admin",
              workflow_run_id: run.run_id,
            });
            break;
          } catch (e) {
            if (String(e).toLowerCase().includes("persisted") && attempt < 9) {
              await new Promise((r) => setTimeout(r, 2000));
              continue;
            }
            throw e;
          }
        }

        console.log(`Profile: ${profile.browser_profile_id}`);
      }

      main();
      ```
    </CodeGroup>
  </Tab>
</Tabs>

<Note>
  `persist_browser_session` is an agent definition property. Set it when creating the agent, not when running it. See [Browser Profiles](/developers/optimization/browser-profiles) for the full lifecycle.
</Note>

***

## Extract patient demographics

Navigate to **Patient/Client > Finder** and extract the results table.

<Tabs>
  <Tab title="Cloud UI">
    Create an agent with two blocks:

    1. **Navigation** block, URL: `https://demo.openemr.io/openemr/index.php`. Goal: "Click Patient/Client in the top menu, then click Finder. Click Search to display all patients."
    2. **Extraction** block, Goal: "Extract all patient rows from the Patient Finder results table." Paste the patient schema into **Data Schema**.

    On the **Start** node, set **Proxy Location** to a country (e.g., **United States**). Run the agent.
  </Tab>

  <Tab title="API / SDK">
    <CodeGroup>
      ```python Python theme={null}
      import asyncio
      from skyvern import Skyvern

      PATIENT_SCHEMA = {
          "type": "object",
          "properties": {
              "patients": {
                  "type": "array",
                  "description": "Patient rows from the Patient Finder results table",
                  "items": {
                      "type": "object",
                      "properties": {
                          "name": {"type": "string", "description": "Patient full name (Last, First)"},
                          "pid": {"type": "string", "description": "Patient ID number"},
                          "dob": {"type": "string", "description": "Date of birth (YYYY-MM-DD)"},
                          "phone_home": {"type": "string", "description": "Home phone number"},
                      },
                  },
              },
          },
      }

      async def main():
          client = Skyvern(api_key="YOUR_API_KEY")

          run = await client.run_task(
              url="https://demo.openemr.io/openemr/index.php",
              prompt=(
                  "Click Patient/Client in the top menu, then click Finder. "
                  "Click Search to display all patients."
              ),
              data_extraction_schema=PATIENT_SCHEMA,
              proxy_location="RESIDENTIAL",
              browser_session_id="YOUR_SESSION_ID",
          )

          while run.status not in ["completed", "failed", "terminated", "timed_out", "canceled"]:
              await asyncio.sleep(5)
              run = await client.get_run(run.run_id)

          print(run.output)

      asyncio.run(main())
      ```

      ```typescript TypeScript theme={null}
      import { Skyvern } from "@skyvern/client";

      const PATIENT_SCHEMA = {
        type: "object",
        properties: {
          patients: {
            type: "array",
            description: "Patient rows from the Patient Finder results table",
            items: {
              type: "object",
              properties: {
                name: { type: "string", description: "Patient full name (Last, First)" },
                pid: { type: "string", description: "Patient ID number" },
                dob: { type: "string", description: "Date of birth (YYYY-MM-DD)" },
                phone_home: { type: "string", description: "Home phone number" },
              },
            },
          },
        },
      } as const;

      async function main() {
        const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });

        let run = await client.runTask({
          body: {
            url: "https://demo.openemr.io/openemr/index.php",
            prompt:
              "Click Patient/Client in the top menu, then click Finder. " +
              "Click Search to display all patients.",
            data_extraction_schema: PATIENT_SCHEMA,
            proxy_location: "RESIDENTIAL",
            browser_session_id: "YOUR_SESSION_ID",
          },
        });

        while (!["completed", "failed", "terminated", "timed_out", "canceled"].includes(run.status)) {
          await new Promise((r) => setTimeout(r, 5000));
          run = await client.getRun(run.run_id);
        }

        console.log(JSON.stringify(run.output, null, 2));
      }

      main();
      ```
    </CodeGroup>
  </Tab>
</Tabs>

**Example output:**

```json theme={null}
{
  "patients": [
    { "name": "Belford, Phil", "pid": "1", "dob": "1972-02-09", "phone_home": "333-444-2222" },
    { "name": "Underwood, Susan Ardmore", "pid": "2", "dob": "1967-02-08", "phone_home": "4443332222" },
    { "name": "Moore, Wanda", "pid": "3", "dob": "2007-02-18", "phone_home": null }
  ]
}
```

<Note>
  The demo resets daily and community users add test patients, so exact records may differ.
</Note>

<Note>
  Browser profiles cannot be used directly with standalone tasks. Create a [browser session](/developers/optimization/browser-sessions) from the profile first, then pass the session ID. See [Pagination with browser sessions](#pagination-with-browser-sessions) below for the full pattern.
</Note>

***

## Extract encounter billing data

Navigate to **Reports > Visits > Superbill**, set a date range, and extract the report.

<Tabs>
  <Tab title="Cloud UI">
    Create an agent with two blocks:

    1. **Navigation** block, Goal: "Click Reports in the top menu, then Visits, then Superbill. Set the From date to 2020-01-01 and the To date to today. Click Submit."
    2. **Extraction** block, Goal: "Extract all encounter rows from the Superbill report." Paste the encounter schema into **Data Schema**.

    On the **Start** node, set **Proxy Location** to a country (e.g., **United States**). Run the agent.
  </Tab>

  <Tab title="API / SDK">
    <CodeGroup>
      ```python Python theme={null}
      ENCOUNTER_SCHEMA = {
          "type": "object",
          "properties": {
              "encounters": {
                  "type": "array",
                  "description": "Encounter rows from the Superbill report",
                  "items": {
                      "type": "object",
                      "properties": {
                          "patient_name": {"type": "string", "description": "Patient name"},
                          "encounter_date": {"type": "string", "description": "Date of encounter (YYYY-MM-DD)"},
                          "provider": {"type": "string", "description": "Provider name"},
                          "billing_code": {"type": "string", "description": "CPT or billing code"},
                          "code_description": {"type": "string", "description": "Description of the billing code"},
                          "charge": {"type": "number", "description": "Fee amount in USD"},
                      },
                  },
              },
          },
      }

      run = await client.run_task(
          url="https://demo.openemr.io/openemr/index.php",
          prompt=(
              "Click Reports in the top menu, then Visits, then Superbill. "
              "Set the From date to 2020-01-01 and the To date to today. Click Submit."
          ),
          data_extraction_schema=ENCOUNTER_SCHEMA,
          proxy_location="RESIDENTIAL",
          browser_session_id="YOUR_SESSION_ID",
      )
      ```

      ```typescript TypeScript theme={null}
      const ENCOUNTER_SCHEMA = {
        type: "object",
        properties: {
          encounters: {
            type: "array",
            description: "Encounter rows from the Superbill report",
            items: {
              type: "object",
              properties: {
                patient_name: { type: "string", description: "Patient name" },
                encounter_date: { type: "string", description: "Date of encounter (YYYY-MM-DD)" },
                provider: { type: "string", description: "Provider name" },
                billing_code: { type: "string", description: "CPT or billing code" },
                code_description: { type: "string", description: "Description of the billing code" },
                charge: { type: "number", description: "Fee amount in USD" },
              },
            },
          },
        },
      } as const;

      let run = await client.runTask({
        body: {
          url: "https://demo.openemr.io/openemr/index.php",
          prompt:
            "Click Reports in the top menu, then Visits, then Superbill. " +
            "Set the From date to 2020-01-01 and the To date to today. Click Submit.",
          data_extraction_schema: ENCOUNTER_SCHEMA,
          proxy_location: "RESIDENTIAL",
          browser_session_id: "YOUR_SESSION_ID",
        },
      });
      ```
    </CodeGroup>
  </Tab>
</Tabs>

**Example output:**

```json theme={null}
{
  "encounters": [
    {
      "patient_name": "Phil Lopez",
      "encounter_date": "2024-06-01",
      "provider": "Administrator Administrator",
      "billing_code": "99213",
      "code_description": "Office/outpatient visit, est patient, low complexity",
      "charge": 50.00
    }
  ]
}
```

***

## Pagination with browser sessions

A [Browser Profile](/developers/optimization/browser-profiles) is a saved snapshot. A [Browser Session](/developers/optimization/browser-sessions) is a live browser instance that persists between tasks. Use sessions to paginate: extract page 1, click Next, extract page 2.

<Tabs>
  <Tab title="Cloud UI">
    <Steps>
      <Step title="Create a session">
        Go to **Browsers** in the sidebar. Click **Create Session**. Set **Proxy Location** to a country (e.g., **United States**) and configure the timeout.

        <Frame>
          <video controls muted playsInline className="w-full aspect-video rounded-xl" src="https://mintcdn.com/skyvern/r7-Gpk9XzCfyJ6Ad/images/browser-session-create.mp4?fit=max&auto=format&n=r7-Gpk9XzCfyJ6Ad&q=85&s=2cbdd336187d0b832722fbe85fbae2f1" data-path="images/browser-session-create.mp4" />
        </Frame>
      </Step>

      <Step title="Navigate and extract page 1">
        Run a task against the session: "Click Patient/Client > Finder. Click Search. Extract all patient rows."
      </Step>

      <Step title="Extract subsequent pages">
        Run another task against the same session: "Click Next to go to the next page. Extract all patient rows." Repeat until no more results.
      </Step>
    </Steps>
  </Tab>

  <Tab title="API / SDK">
    <CodeGroup>
      ```python Python theme={null}
      import asyncio
      from skyvern import Skyvern

      PATIENT_SCHEMA = {
          "type": "object",
          "properties": {
              "patients": {
                  "type": "array",
                  "description": "Patient rows from the current page",
                  "items": {
                      "type": "object",
                      "properties": {
                          "name": {"type": "string", "description": "Patient full name (Last, First)"},
                          "pid": {"type": "string", "description": "Patient ID number"},
                          "dob": {"type": "string", "description": "Date of birth (YYYY-MM-DD)"},
                          "phone_home": {"type": "string", "description": "Home phone number"},
                      },
                  },
              },
          },
      }

      async def extract_page(client, session_id, page_num):
          prompt = (
              "Click Patient/Client in the top menu, then click Finder. "
              "Click Search to display all patients. "
              "Extract all patient rows from the results table."
          ) if page_num == 1 else (
              "Click Next to go to the next page of results. "
              "Extract all patient rows from the table."
          )

          run = await client.run_task(
              url="https://demo.openemr.io/openemr/index.php",
              prompt=prompt,
              browser_session_id=session_id,
              data_extraction_schema=PATIENT_SCHEMA,
          )

          while run.status not in ["completed", "failed", "terminated", "timed_out", "canceled"]:
              await asyncio.sleep(5)
              run = await client.get_run(run.run_id)

          return run

      async def main():
          client = Skyvern(api_key="YOUR_API_KEY")

          session = await client.create_browser_session(
              browser_profile_id="YOUR_PROFILE_ID",
              proxy_location="RESIDENTIAL",
          )

          all_patients = []
          for page in range(1, 11):
              run = await extract_page(client, session.browser_session_id, page)

              if run.status != "completed":
                  break

              patients = run.output.get("patients", [])
              if not patients:
                  break

              all_patients.extend(patients)
              print(f"Page {page}: {len(patients)} patients ({len(all_patients)} total)")

          print(f"Done: {len(all_patients)} patients")

      asyncio.run(main())
      ```

      ```typescript TypeScript theme={null}
      import { Skyvern } from "@skyvern/client";

      const PATIENT_SCHEMA = {
        type: "object",
        properties: {
          patients: {
            type: "array",
            description: "Patient rows from the current page",
            items: {
              type: "object",
              properties: {
                name: { type: "string", description: "Patient full name (Last, First)" },
                pid: { type: "string", description: "Patient ID number" },
                dob: { type: "string", description: "Date of birth (YYYY-MM-DD)" },
                phone_home: { type: "string", description: "Home phone number" },
              },
            },
          },
        },
      } as const;

      async function extractPage(client: Skyvern, sessionId: string, pageNum: number) {
        const prompt =
          pageNum === 1
            ? "Click Patient/Client in the top menu, then click Finder. " +
              "Click Search to display all patients. " +
              "Extract all patient rows from the results table."
            : "Click Next to go to the next page of results. " +
              "Extract all patient rows from the table.";

        let run = await client.runTask({
          body: {
            url: "https://demo.openemr.io/openemr/index.php",
            prompt,
            browser_session_id: sessionId,
            data_extraction_schema: PATIENT_SCHEMA,
          },
        });

        while (!["completed", "failed", "terminated", "timed_out", "canceled"].includes(run.status)) {
          await new Promise((r) => setTimeout(r, 5000));
          run = await client.getRun(run.run_id);
        }

        return run;
      }

      async function main() {
        const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });

        const session = await client.createBrowserSession({
          browser_profile_id: "YOUR_PROFILE_ID",
          proxy_location: "RESIDENTIAL",
        });

        const allPatients: any[] = [];
        for (let page = 1; page <= 10; page++) {
          const run = await extractPage(client, session.browser_session_id, page);

          if (run.status !== "completed") break;

          const patients = run.output?.patients ?? [];
          if (patients.length === 0) break;

          allPatients.push(...patients);
          console.log(`Page ${page}: ${patients.length} patients (${allPatients.length} total)`);
        }

        console.log(`Done: ${allPatients.length} patients`);
      }

      main();
      ```
    </CodeGroup>

    **Expected output:**

    ```
    Page 1: 25 patients (25 total)
    Page 2: 18 patients (43 total)
    Done: 43 patients
    ```
  </Tab>
</Tabs>

<Warning>
  You cannot use `browser_profile_id` and `browser_session_id` in the same request. Use the profile to create the session, then pass only the session ID to tasks.
</Warning>

***

## Error handling

OpenEMR can timeout or show session-expired pages. Use `error_code_mapping` on agent blocks to classify failures, and `max_retries` to retry automatically.

<Tabs>
  <Tab title="Cloud UI">
    On each Navigation and Extraction block, expand **Advanced Settings** and enable **Error Messages**. Add this JSON:

    ```json theme={null}
    { "session_expired": "Session expired, login required, or access denied page" }
    ```

    <Frame>
      <video controls muted playsInline className="w-full aspect-video rounded-xl" src="https://mintcdn.com/skyvern/17jBkYyUA0bbEAFE/images/navigation-error-messages.mp4?fit=max&auto=format&n=17jBkYyUA0bbEAFE&q=85&s=5f8758dc903fcdb673c704d4679f672f" data-path="images/navigation-error-messages.mp4" />
    </Frame>

    <Note>
      `max_retries` is only available via the API. In the Cloud UI, Skyvern uses its default retry behavior. For fine-grained retry control, use the API/SDK approach.
    </Note>
  </Tab>

  <Tab title="API / SDK">
    Set `error_code_mapping` and `max_retries` directly on agent blocks:

    ```json theme={null}
    {
      "block_type": "extraction",
      "label": "extract_patients",
      "data_extraction_goal": "Extract all patient rows from the table",
      "error_code_mapping": {
        "session_expired": "Session expired, login required, or access denied page"
      },
      "max_retries": 3
    }
    ```

    For standalone tasks, handle retries in your calling code:

    <CodeGroup>
      ```python Python theme={null}
      async def run_with_retry(client, session_id, page_num, max_retries=3):
          for attempt in range(max_retries + 1):
              run = await extract_page(client, session_id, page_num)
              if run.status == "completed":
                  return run

              is_session_error = "session" in (run.failure_reason or "").lower()
              if is_session_error and attempt < max_retries:
                  await asyncio.sleep(2 ** attempt * 5)
                  continue

              return run
          return run
      ```

      ```typescript TypeScript theme={null}
      async function runWithRetry(
        client: Skyvern, sessionId: string, pageNum: number, maxRetries = 3
      ) {
        let run;
        for (let attempt = 0; attempt <= maxRetries; attempt++) {
          run = await extractPage(client, sessionId, pageNum);
          if (run.status === "completed") return run;

          const isSessionError = (run.failure_reason ?? "").toLowerCase().includes("session");
          if (isSessionError && attempt < maxRetries) {
            await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 5000));
            continue;
          }
          return run;
        }
        return run!;
      }
      ```
    </CodeGroup>
  </Tab>
</Tabs>

See [Error Handling](/developers/going-to-production/error-handling) and [CAPTCHA & Bot Bypass](/developers/features/captcha-and-bot-bypass) for more.

***

## Complete agent

This agent combines everything: navigate to the Patient Finder, extract demographics, navigate to Superbill, and extract billing data, with error recovery and residential proxy.

<Tabs>
  <Tab title="Cloud UI">
    <Steps>
      <Step title="Create the agent">
        Go to **Agents** and create a new agent named "OpenEMR Daily Extract." On the **Start** node, enable **Save & Reuse Session** and set **Proxy Location** to a country (e.g., **United States**).
      </Step>

      <Step title="Block 1: Navigation (open Patient Finder)">
        Add a **Navigation** block. Set URL to `https://demo.openemr.io/openemr/index.php` and goal: "Click Patient/Client > Finder. Click Search to display all patients. If a login page appears, log in with 'admin'/'pass'." In **Advanced Settings**, enable **Error Messages** and add `{"session_expired": "Session expired, login required, or access denied page"}`.
      </Step>

      <Step title="Block 2: Extraction (patient demographics)">
        Add an **Extraction** block. Set goal: "Extract all patient rows from the Patient Finder results table." Paste the patient schema into **Data Schema**.
      </Step>

      <Step title="Block 3: Navigation (open Superbill)">
        Add another **Navigation** block. Set goal: "Click Reports > Visits > Superbill. Set From to 2020-01-01, To to today. Click Submit." Add the same error messages mapping.
      </Step>

      <Step title="Block 4: Extraction (encounter billing)">
        Add another **Extraction** block. Set goal: "Extract all encounter rows from the Superbill report." Paste the encounter schema into **Data Schema**.
      </Step>

      <Step title="Run">
        Click **Run**. The agent navigates to the Patient Finder, extracts demographics, then navigates to Superbill and extracts billing data.
      </Step>
    </Steps>

    For multi-page results, combine with the [pagination pattern](#pagination-with-browser-sessions) above.
  </Tab>

  <Tab title="API / SDK">
    <CodeGroup>
      ```python Python theme={null}
      import asyncio
      from skyvern import Skyvern

      PATIENT_SCHEMA = {
          "type": "object",
          "properties": {
              "patients": {
                  "type": "array",
                  "description": "Patient rows from the Patient Finder results table",
                  "items": {
                      "type": "object",
                      "properties": {
                          "name": {"type": "string", "description": "Patient full name (Last, First)"},
                          "pid": {"type": "string", "description": "Patient ID number"},
                          "dob": {"type": "string", "description": "Date of birth (YYYY-MM-DD)"},
                          "phone_home": {"type": "string", "description": "Home phone number"},
                      },
                  },
              },
          },
      }

      ENCOUNTER_SCHEMA = {
          "type": "object",
          "properties": {
              "encounters": {
                  "type": "array",
                  "description": "Encounter rows from the Superbill report",
                  "items": {
                      "type": "object",
                      "properties": {
                          "patient_name": {"type": "string", "description": "Patient name"},
                          "encounter_date": {"type": "string", "description": "Date of encounter (YYYY-MM-DD)"},
                          "provider": {"type": "string", "description": "Provider name"},
                          "billing_code": {"type": "string", "description": "CPT or billing code"},
                          "code_description": {"type": "string", "description": "Description of the billing code"},
                          "charge": {"type": "number", "description": "Fee amount in USD"},
                      },
                  },
              },
          },
      }

      SESSION_ERROR = "Session expired, login required, or access denied page"

      async def main():
          client = Skyvern(api_key="YOUR_API_KEY")

          workflow = await client.create_workflow(
              json_definition={
                  "title": "OpenEMR Daily Extract",
                  "persist_browser_session": True,
                  "workflow_definition": {
                      "parameters": [],
                      "blocks": [
                          {
                              "block_type": "navigation",
                              "label": "open_patient_finder",
                              "url": "https://demo.openemr.io/openemr/index.php",
                              "navigation_goal": (
                                  "Click Patient/Client in the top menu, then click Finder. "
                                  "Click Search to display all patients. "
                                  "If a login page appears, log in with username 'admin' and password 'pass'."
                              ),
                              "error_code_mapping": {"session_expired": SESSION_ERROR},
                              "max_retries": 3,
                          },
                          {
                              "block_type": "extraction",
                              "label": "extract_patients",
                              "data_extraction_goal": "Extract all patient rows from the Patient Finder results table",
                              "data_schema": PATIENT_SCHEMA,
                              "error_code_mapping": {"session_expired": SESSION_ERROR},
                              "max_retries": 2,
                          },
                          {
                              "block_type": "navigation",
                              "label": "open_superbill",
                              "navigation_goal": (
                                  "Click Reports in the top menu, then Visits, then Superbill. "
                                  "Set the From date to 2020-01-01 and the To date to today. Click Submit."
                              ),
                              "error_code_mapping": {"session_expired": SESSION_ERROR},
                              "max_retries": 3,
                          },
                          {
                              "block_type": "extraction",
                              "label": "extract_encounters",
                              "data_extraction_goal": "Extract all encounter rows from the Superbill report",
                              "data_schema": ENCOUNTER_SCHEMA,
                              "error_code_mapping": {"session_expired": SESSION_ERROR},
                              "max_retries": 2,
                          },
                      ],
                  },
              }
          )
          print(f"Workflow: {workflow.workflow_permanent_id}")

          run = await client.run_workflow(
              workflow_id=workflow.workflow_permanent_id,
              browser_profile_id="YOUR_PROFILE_ID",
              proxy_location="RESIDENTIAL",
              wait_for_completion=True,
          )

          print(f"Status: {run.status}")
          print(f"Output: {run.output}")

      asyncio.run(main())
      ```

      ```typescript TypeScript theme={null}
      import { Skyvern } from "@skyvern/client";

      const PATIENT_SCHEMA = {
        type: "object",
        properties: {
          patients: {
            type: "array",
            description: "Patient rows from the Patient Finder results table",
            items: {
              type: "object",
              properties: {
                name: { type: "string", description: "Patient full name (Last, First)" },
                pid: { type: "string", description: "Patient ID number" },
                dob: { type: "string", description: "Date of birth (YYYY-MM-DD)" },
                phone_home: { type: "string", description: "Home phone number" },
              },
            },
          },
        },
      } as const;

      const ENCOUNTER_SCHEMA = {
        type: "object",
        properties: {
          encounters: {
            type: "array",
            description: "Encounter rows from the Superbill report",
            items: {
              type: "object",
              properties: {
                patient_name: { type: "string", description: "Patient name" },
                encounter_date: { type: "string", description: "Date of encounter (YYYY-MM-DD)" },
                provider: { type: "string", description: "Provider name" },
                billing_code: { type: "string", description: "CPT or billing code" },
                code_description: { type: "string", description: "Description of the billing code" },
                charge: { type: "number", description: "Fee amount in USD" },
              },
            },
          },
        },
      } as const;

      const SESSION_ERROR = "Session expired, login required, or access denied page";

      async function main() {
        const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });

        const workflow = await client.createWorkflow({
          body: {
            json_definition: {
              title: "OpenEMR Daily Extract",
              persist_browser_session: true,
              workflow_definition: {
                parameters: [],
                blocks: [
                  {
                    block_type: "navigation",
                    label: "open_patient_finder",
                    url: "https://demo.openemr.io/openemr/index.php",
                    navigation_goal:
                      "Click Patient/Client in the top menu, then click Finder. " +
                      "Click Search to display all patients. " +
                      "If a login page appears, log in with username 'admin' and password 'pass'.",
                    error_code_mapping: { session_expired: SESSION_ERROR },
                    max_retries: 3,
                  },
                  {
                    block_type: "extraction",
                    label: "extract_patients",
                    data_extraction_goal: "Extract all patient rows from the Patient Finder results table",
                    data_schema: PATIENT_SCHEMA,
                    error_code_mapping: { session_expired: SESSION_ERROR },
                    max_retries: 2,
                  },
                  {
                    block_type: "navigation",
                    label: "open_superbill",
                    navigation_goal:
                      "Click Reports in the top menu, then Visits, then Superbill. " +
                      "Set the From date to 2020-01-01 and the To date to today. Click Submit.",
                    error_code_mapping: { session_expired: SESSION_ERROR },
                    max_retries: 3,
                  },
                  {
                    block_type: "extraction",
                    label: "extract_encounters",
                    data_extraction_goal: "Extract all encounter rows from the Superbill report",
                    data_schema: ENCOUNTER_SCHEMA,
                    error_code_mapping: { session_expired: SESSION_ERROR },
                    max_retries: 2,
                  },
                ],
              },
            },
          },
        });
        console.log(`Workflow: ${workflow.workflow_permanent_id}`);

        const run = await client.runWorkflow({
          body: {
            workflow_id: workflow.workflow_permanent_id,
            browser_profile_id: "YOUR_PROFILE_ID",
            proxy_location: "RESIDENTIAL",
          },
          waitForCompletion: true,
        });

        console.log(`Status: ${run.status}`);
        console.log(`Output: ${JSON.stringify(run.output, null, 2)}`);
      }

      main();
      ```
    </CodeGroup>
  </Tab>
</Tabs>

| Technique               | Purpose                                  |
| ----------------------- | ---------------------------------------- |
| Residential proxy       | Bypass WAF/bot detection                 |
| Browser profile         | Skip login on every run                  |
| Navigation goals        | Explicit menu clicks for iframe-based UI |
| JSON schemas            | Consistent, structured output            |
| Session reuse           | Paginate multi-page results              |
| Error mapping + retries | Recover from session timeouts            |

<Tip>
  The OpenEMR demo resets daily at 8:00 AM UTC, so profiles expire every day. In production, re-run your login agent weekly or whenever extractions fail with auth errors. See [Browser Profiles](/developers/optimization/browser-profiles) for the refresh pattern.
</Tip>

***

## Resources

<CardGroup cols={2}>
  <Card title="Browser Profiles" icon="user" href="/developers/optimization/browser-profiles">
    Full lifecycle: create, refresh, and delete saved browser state
  </Card>

  <Card title="Proxy & Geolocation" icon="globe" href="/developers/going-to-production/proxy-geolocation">
    All proxy locations and country-specific routing options
  </Card>

  <Card title="Credential Management" icon="key" href="/sdk-reference/credentials/create-credential">
    Securely store and use login credentials
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/developers/going-to-production/error-handling">
    Error code mapping, failure classification, and retry strategies
  </Card>

  <Card title="Extract Structured Data" icon="table" href="/running-automations/extract-structured-data">
    JSON schema design and the interactive schema builder
  </Card>
</CardGroup>
