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

> ## Agent Instructions
> To search or read these docs, connect the Ringg docs MCP server at https://docs.ringg.ai/mcp (no key needed; tools search_ringg_ai and query_docs_filesystem_ringg_ai) instead of browsing pages one by one. Setup for each client: https://docs.ringg.ai/resources/mcp-server/docs-mcp-server/mcp-for-ai-assistants.md
> The dashboard labels agents "Assistants"; these docs say "agent". Both mean the same object, identified by agent_id.
> API base URL: https://prod-api.ringg.ai/ca/api/v0. Send the workspace API key in the X-API-KEY header, from server-side code only.
> Placing a call dials a real phone number and spends credits. Confirm the number with the user before calling.
> To act on a Ringg workspace (place calls, edit agents, read transcripts), use the separate Ringg workspace MCP server at https://mcp.ringg.ai/mcp: https://docs.ringg.ai/resources/mcp-server/workspace-mcp-server/overview.md

# Lesson: Calling lists

> Build a campaign CSV with a column for each of Ananya's variables, map the CRM's headers by hand, read the upload summary and fix an invalid row.

export const Quiz = ({question, answers, correctFeedback, incorrectFeedback}) => {
  const [selected, setSelected] = React.useState(null);
  const [checked, setChecked] = React.useState(false);
  const quizId = React.useId();
  const isCorrect = checked && answers[selected]?.correct;
  const reset = () => {
    setSelected(null);
    setChecked(false);
  };
  const optionStyle = (answer, i) => {
    const base = {
      display: "flex",
      alignItems: "flex-start",
      gap: "0.6rem",
      padding: "0.6rem 0.8rem",
      marginTop: "0.5rem",
      borderRadius: "0.6rem",
      border: "1px solid rgba(128, 128, 128, 0.35)",
      cursor: checked ? "default" : "pointer",
      lineHeight: 1.45
    };
    if (checked && answer.correct) {
      return {
        ...base,
        borderColor: "rgba(22, 163, 74, 0.8)",
        background: "rgba(22, 163, 74, 0.12)"
      };
    }
    if (checked && selected === i && !answer.correct) {
      return {
        ...base,
        borderColor: "rgba(220, 38, 38, 0.8)",
        background: "rgba(220, 38, 38, 0.1)"
      };
    }
    if (!checked && selected === i) {
      return {
        ...base,
        borderColor: "currentColor",
        background: "rgba(128, 128, 128, 0.12)"
      };
    }
    return base;
  };
  const buttonStyle = disabled => ({
    marginTop: "0.9rem",
    padding: "0.45rem 0.9rem",
    borderRadius: "0.5rem",
    border: "1px solid rgba(128, 128, 128, 0.5)",
    background: "rgba(128, 128, 128, 0.1)",
    color: "inherit",
    font: "inherit",
    fontSize: "0.9em",
    cursor: disabled ? "not-allowed" : "pointer",
    opacity: disabled ? 0.5 : 1
  });
  return <div style={{
    marginTop: "1.5rem",
    padding: "1rem 1.1rem",
    borderRadius: "0.8rem",
    border: "1px solid rgba(128, 128, 128, 0.35)"
  }}>
      <Badge color="green">Quiz</Badge>
      <p id={quizId + "-question"} style={{
    fontWeight: 600,
    margin: "0.6rem 0 0.2rem"
  }}>
        {question}
      </p>
      <div role="radiogroup" aria-labelledby={quizId + "-question"} aria-disabled={checked}>
        {answers.map((answer, i) => <label key={i} style={optionStyle(answer, i)}>
            <input type="radio" name={"quiz-" + quizId} checked={selected === i} onChange={() => !checked && setSelected(i)} disabled={checked} style={{
    marginTop: "0.3rem",
    flexShrink: 0
  }} />
            <span>{answer.text}</span>
          </label>)}
      </div>
      {checked && <div role="status" aria-live="polite" aria-atomic="true" style={{
    marginTop: "0.9rem",
    padding: "0.7rem 0.9rem",
    borderRadius: "0.6rem",
    lineHeight: 1.5,
    background: isCorrect ? "rgba(22, 163, 74, 0.12)" : "rgba(220, 38, 38, 0.1)"
  }}>
          <strong style={{
    marginRight: "0.4rem"
  }}>{isCorrect ? "✓" : "✗"}</strong>
          {isCorrect ? correctFeedback : incorrectFeedback}
        </div>}
      {!checked ? <button type="button" onClick={() => selected !== null && setChecked(true)} disabled={selected === null} style={buttonStyle(selected === null)}>
          Check answer
        </button> : <button type="button" onClick={reset} style={buttonStyle(false)}>
          Try again
        </button>}
    </div>;
};

export const RelatedTopics = ({items}) => <div className="not-prose mt-12">
    <div className="text-sm font-medium text-gray-500 dark:text-zinc-400 mb-3">Related topics</div>
    <div className="flex flex-col gap-2.5">
      {items.map(([title, href]) => <a key={href} href={href} className="flex items-center gap-2.5 text-sm text-gray-700 hover:text-gray-950 dark:text-zinc-300 dark:hover:text-white">
          <Icon icon="file-text" size={16} color="currentColor" />
          {title}
        </a>)}
    </div>
  </div>;

**Part 4: Taking your agent live · Lesson 3 of 4 · 15 minutes**

## Objective

Produce a CSV that fills every one of Ananya's variables, and find and fix a row Ringg rejects.

## The scenario

Acme's CRM exports the customers due on 15 October with its own headers: `Customer`, `Phone`, `EMI`, `Due` and `Loan A/c`. Nothing matches a variable name, so you map the columns by hand, and one phone number in the export is incomplete. Use your own and your colleagues' numbers.

| Customer | Phone | EMI | Due | Loan A/c |
| - | - | - | - | - |
| Rahul Verma | your number | "₹4,500" | 15 October | LN-20481 |
| Priya Sharma | a colleague's number | "₹1,200" | 15 October | LN-20493 |
| Ananya Iyer | a number missing its last three digits | "₹8,999" | 15 October | LN-20517 |

Phone numbers are ten digits without `+91`; the amounts sit in double quotes because they contain a comma.

## Hands-on

<Frame caption="Uploading and mapping a calling list">
  <video alt="Uploading and mapping a calling list" aria-label="Uploading and mapping a calling list" controls muted playsInline className="w-full rounded-xl" style={{ aspectRatio: "16 / 10" }} preload="metadata" controlsList="nodownload noremoteplayback" poster="/media/campaigns-calling-list/poster.webp">
    <source src="https://mintcdn.com/ringgai-0da73d20/zhzhS2waLZvF8qXk/media/campaigns-calling-list/video.webm?fit=max&auto=format&n=zhzhS2waLZvF8qXk&q=85&s=fae0887ef08b35828c2e3bf7b1a1b72f" type="video/webm; codecs=&#x22;av01.0.09M.08&#x22;" data-path="media/campaigns-calling-list/video.webm" />
  </video>
</Frame>

<Steps>
  <Step title="Upload the export">
    Open **Monitor → Campaigns → Create campaign**. Name it `EMI reminders - 15 October`, pick **EMI reminder - Hindi**, and drop the CSV on **Upload calling list**. The file must be `.csv`, up to 20 MB, with a header row.
  </Step>

  <Step title="Map the columns">
    In **Calling list settings**, keep **Country code** **+91**. Under **Map variables to CSV columns**, every variable reads **Select CSV column**. Pick `Customer` for `callee_name`, `Phone` for `mobile_number`, `EMI` for `due_amount`, `Due` for `due_date` and `Loan A/c` for `loan_account`; **Next** stays disabled until all five are mapped. Leave **Remove invalid rows** on and **Transliterate variables** off: it rewrites values into the agent's primary language, English (India) for Ananya.
  </Step>

  <Step title="Read the upload summary">
    Click **Next**. After "CSV verified successfully", step 2 shows **Total rows** 3, **Valid contacts** 2, **Successful upload** `2/3 uploaded` and **Invalid rows** 1 with **Download invalid rows**.
  </Step>

  <Step title="Fix the row">
    Download the rejected rows (`invalid_entries.csv`), complete the phone number and put the row back. While editing, rename the headers to the variable names so the next upload maps itself; matching is exact, including case.
  </Step>

  <Step title="Upload again">
    Leave the page without launching: the campaign stays in the list as a **Draft**, where you can select it and click **Terminate**. Create a new campaign with the fixed file and click **Next**.
  </Step>
</Steps>

## Checkpoint

<Check>
  Every variable is mapped before you touch a dropdown, and the summary reads **Total rows** 3, **Valid contacts** 3, **Successful upload** `3/3 uploaded`, with no **Invalid rows**.
</Check>

## Exercise

Upload the original export once more with **Remove invalid rows** switched off. Expected result: the upload still loads the two valid rows and sets the bad one aside, and Ringg also emails you the rejected rows. Keep the switch on for real lists to skip that email.

## What you learned

* A list needs one column per agent variable, always including `mobile_number` and `callee_name`. Headers that equal the variable name map themselves; anything else you map by hand.
* One **Country code** covers the whole list, numbers are national-format digits, and another country means another campaign.
* Rows Ringg can't call are dropped and returned as `invalid_entries.csv`; with **Remove invalid rows** off, they are also emailed to you. A campaign where every row fails ends as **No Valid Contacts**.

<Quiz
  question="The CRM export has the headers Customer, Phone, EMI, Due and Loan A/c. What does the campaign form do with them?"
  answers={[
{ text: "Rejects the file; headers must equal the variable names", correct: false },
{ text: "Maps them by position: first column to callee_name, second to mobile_number", correct: false },
{ text: "Leaves every variable on Select CSV column until you map each one by hand", correct: true },
{ text: "Maps Customer and Phone automatically and ignores the rest", correct: false },
]}
  correctFeedback="Right. Headers that equal a variable name map themselves; matching is exact, including case. Anything else waits on Select CSV column, and Next stays disabled until every variable has a column."
  incorrectFeedback="Not quite. Nothing is rejected and nothing is guessed by position. A header that exactly equals a variable name maps itself; every other variable reads Select CSV column until you map it, and Next is disabled until all are mapped."
/>

<RelatedTopics items={[["Calling list (CSV)", "/documentation/deploy/campaigns/calling-list"], ["Variables", "/documentation/build-agents/customize-agent-behaviour/custom-variables"]]} />


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.