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

# Part 2: Content Lifecycle

> Update, bulk-edit, and delete Data Engine content safely — scoped to exactly the items you created.

This is **Part 2 of the Build an End-to-End RAG Pipeline series**. In Part 1 you ingested content and verified it was indexed. Real content doesn't stay still: titles get corrected, you re-tag in bulk, and you remove outdated material. This part covers those lifecycle operations — and how to run them safely against a publisher that may also hold content you don't want to touch.

The key idea is **scoping**: every edit and delete here targets the exact items this recipe created, identified by the item IDs you captured when you uploaded them — never the whole publisher.

## Pipeline at a Glance

<Steps>
  <Step title="Publisher setup (Studio)">
    Create the publisher that owns your content — [Part 1](/tutorials/rag-pipeline-part-1).
  </Step>

  <Step title="Ingest content with metadata">
    Upload files and enrich them — [Part 1](/tutorials/rag-pipeline-part-1).
  </Step>

  <Step title="Verify indexing">
    Poll item status until your content is searchable — [Part 1](/tutorials/rag-pipeline-part-1).
  </Step>

  <Step title="Semantic search">
    Query your content — deep dive: [Building Custom Search](/tutorials/search).
  </Step>

  <Step title="Grounded completions with sources">
    Answer questions from your content with citations — deep dive: [Grounded Completions with RAG](/tutorials/completions-grounded).
  </Step>

  <Step title="Content lifecycle">
    Update, bulk-edit, and delete content — **covered below**.
  </Step>

  <Step title="Verification, errors & resilience">
    Error handling and retry patterns — [Part 3](/tutorials/rag-pipeline-part-3).
  </Step>
</Steps>

## Prerequisites

Before starting, ensure you have:

* A Gloo AI Studio account
* Your Client ID and Client Secret from the [API Credentials page](/studio/manage-api-credentials)
* A **publisher** with credentials — see [Part 1](/tutorials/rag-pipeline-part-1) to create one
* Familiarity with uploading and indexing content from [Part 1](/tutorials/rag-pipeline-part-1)

<Info>
  This recipe seeds its own sample content, so it runs standalone — you don't need to have completed Part 1 first. It reuses Part 1's upload and indexing patterns for that seeding step.
</Info>

***

## How Scoping Keeps This Safe

A publisher can hold content from many sources. Bulk edits and deletes are powerful, so this recipe never operates publisher-wide. Instead it follows one rule:

> Keep the **item ID** the API returns when you upload each item, and scope every bulk edit and delete to that exact list of item IDs.

The upload response gives you each item's ID directly (Part 1, Step 2). Those IDs are authoritative handles for content you own, so building your bulk operations from them guarantees you only ever touch your own items.

<Note>
  There's also a lookup endpoint, **POST** `/engine/v2/publisher/{publisher_id}/items/by-producer`, that resolves your producer IDs back to item IDs — handy when a separate process only has the producer IDs it assigned. Be aware it's **cached (\~5 minutes) and isn't invalidated when items change**, so right after an upload or delete it can return stale or already-deleted item IDs. Treat the upload response (or a `GET` on the item) as the source of truth; don't rely on `by-producer` immediately after a write.
</Note>

## Step 1: Set Up Sample Content

This recipe manages three short articles, each uploaded under a stable producer ID. The upload response returns each item's ID — capture it; that's the handle every later step uses.

| Producer ID                                | Initial title                   |
| ------------------------------------------ | ------------------------------- |
| `rag-pipeline-part2-volunteer-onboarding`  | Onboarding New Volunteers       |
| `rag-pipeline-part2-measuring-impact`      | Measuring Community Impact      |
| `rag-pipeline-part2-sustaining-engagement` | Sustaining Long-Term Engagement |

The files live in [`sample_files/`](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/rag-pipeline-part-2/sample_files). Upload each one, set its initial metadata, and keep the returned `item_id` in a map keyed by producer ID:

<CodeGroup>
  ```python Python theme={null}
  import requests

  CLIENT_ID = "your_client_id"
  CLIENT_SECRET = "your_client_secret"
  PUBLISHER_ID = "your_publisher_id"

  SEED_ITEMS = [
      {"file": "volunteer-onboarding.md",
       "producer_id": "rag-pipeline-part2-volunteer-onboarding",
       "item_title": "Onboarding New Volunteers",
       "item_tags": ["volunteers", "rag-pipeline-series"]},
      {"file": "measuring-community-impact.md",
       "producer_id": "rag-pipeline-part2-measuring-impact",
       "item_title": "Measuring Community Impact",
       "item_tags": ["measurement", "rag-pipeline-series"]},
      {"file": "sustaining-engagement.md",
       "producer_id": "rag-pipeline-part2-sustaining-engagement",
       "item_title": "Sustaining Long-Term Engagement",
       "item_tags": ["engagement", "rag-pipeline-series"]},
  ]

  # Get an access token (see the Authentication tutorial)
  token = requests.post(
      "https://platform.ai.gloo.com/oauth2/token",
      data={"grant_type": "client_credentials", "scope": "api/access"},
      auth=(CLIENT_ID, CLIENT_SECRET),
  ).json()["access_token"]

  # Upload each file and KEEP the returned item_id — your authoritative handle
  mapping = {}
  for item in SEED_ITEMS:
      with open(f"sample_files/{item['file']}", "rb") as f:
          result = requests.post(
              "https://platform.ai.gloo.com/ingestion/v2/files",
              headers={"Authorization": f"Bearer {token}"},
              params={"producer_id": item["producer_id"]},
              files={"files": (item["file"], f)},
              data={"publisher_id": PUBLISHER_ID},
          ).json()
      item_id = (result["ingesting"] or result["duplicates"])[0]
      mapping[item["producer_id"]] = item_id
      # Set initial metadata (see Part 1, Step 3)
      requests.patch(
          "https://platform.ai.gloo.com/engine/v2/item",
          headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
          json={"publisher_id": PUBLISHER_ID, "item_id": item_id,
                "item_title": item["item_title"], "item_tags": item["item_tags"]},
      )
      print(f"Uploaded {item['producer_id']} -> {item_id}")

  item_ids = list(mapping.values())
  # Wait for every item to finish indexing before editing (see Part 1, Step 4).
  ```

  ```javascript JavaScript theme={null}
  import { readFile } from "node:fs/promises";

  const CLIENT_ID = "your_client_id";
  const CLIENT_SECRET = "your_client_secret";
  const PUBLISHER_ID = "your_publisher_id";

  const SEED_ITEMS = [
    { file: "volunteer-onboarding.md", producer_id: "rag-pipeline-part2-volunteer-onboarding",
      item_title: "Onboarding New Volunteers", item_tags: ["volunteers", "rag-pipeline-series"] },
    { file: "measuring-community-impact.md", producer_id: "rag-pipeline-part2-measuring-impact",
      item_title: "Measuring Community Impact", item_tags: ["measurement", "rag-pipeline-series"] },
    { file: "sustaining-engagement.md", producer_id: "rag-pipeline-part2-sustaining-engagement",
      item_title: "Sustaining Long-Term Engagement", item_tags: ["engagement", "rag-pipeline-series"] },
  ];

  // Get an access token (see the Authentication tutorial)
  const tokenResponse = await fetch("https://platform.ai.gloo.com/oauth2/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`,
    },
    body: new URLSearchParams({ grant_type: "client_credentials", scope: "api/access" }),
  });
  const token = (await tokenResponse.json()).access_token;

  // Upload each file and KEEP the returned item_id — your authoritative handle
  const mapping = {};
  for (const item of SEED_ITEMS) {
    const form = new FormData();
    form.append("publisher_id", PUBLISHER_ID);
    form.append("files", new Blob([await readFile(`sample_files/${item.file}`)]), item.file);
    const result = await (
      await fetch(
        `https://platform.ai.gloo.com/ingestion/v2/files?producer_id=${encodeURIComponent(item.producer_id)}`,
        { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: form }
      )
    ).json();
    const itemId = (result.ingesting.length ? result.ingesting : result.duplicates)[0];
    mapping[item.producer_id] = itemId;
    // Set initial metadata (see Part 1, Step 3)
    await fetch("https://platform.ai.gloo.com/engine/v2/item", {
      method: "PATCH",
      headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
      body: JSON.stringify({ publisher_id: PUBLISHER_ID, item_id: itemId,
        item_title: item.item_title, item_tags: item.item_tags }),
    });
    console.log(`Uploaded ${item.producer_id} -> ${itemId}`);
  }

  const itemIds = Object.values(mapping);
  // Wait for every item to finish indexing before editing (see Part 1, Step 4).
  ```

  ```typescript TypeScript theme={null}
  import { readFile } from "node:fs/promises";

  const CLIENT_ID = "your_client_id";
  const CLIENT_SECRET = "your_client_secret";
  const PUBLISHER_ID = "your_publisher_id";

  interface SeedItem {
    file: string;
    producer_id: string;
    item_title: string;
    item_tags: string[];
  }

  const SEED_ITEMS: SeedItem[] = [
    { file: "volunteer-onboarding.md", producer_id: "rag-pipeline-part2-volunteer-onboarding",
      item_title: "Onboarding New Volunteers", item_tags: ["volunteers", "rag-pipeline-series"] },
    { file: "measuring-community-impact.md", producer_id: "rag-pipeline-part2-measuring-impact",
      item_title: "Measuring Community Impact", item_tags: ["measurement", "rag-pipeline-series"] },
    { file: "sustaining-engagement.md", producer_id: "rag-pipeline-part2-sustaining-engagement",
      item_title: "Sustaining Long-Term Engagement", item_tags: ["engagement", "rag-pipeline-series"] },
  ];

  (async () => {
    // Get an access token (see the Authentication tutorial)
    const tokenResponse = await fetch("https://platform.ai.gloo.com/oauth2/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Authorization: `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`,
      },
      body: new URLSearchParams({ grant_type: "client_credentials", scope: "api/access" }),
    });
    const token = ((await tokenResponse.json()) as { access_token: string }).access_token;

    // Upload each file and KEEP the returned item_id — your authoritative handle
    const mapping: Record<string, string> = {};
    for (const item of SEED_ITEMS) {
      const form = new FormData();
      form.append("publisher_id", PUBLISHER_ID);
      form.append("files", new Blob([await readFile(`sample_files/${item.file}`)]), item.file);
      const result = (await (
        await fetch(
          `https://platform.ai.gloo.com/ingestion/v2/files?producer_id=${encodeURIComponent(item.producer_id)}`,
          { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: form }
        )
      ).json()) as { ingesting: string[]; duplicates: string[] };
      const itemId = (result.ingesting.length ? result.ingesting : result.duplicates)[0];
      mapping[item.producer_id] = itemId;
      // Set initial metadata (see Part 1, Step 3)
      await fetch("https://platform.ai.gloo.com/engine/v2/item", {
        method: "PATCH",
        headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
        body: JSON.stringify({ publisher_id: PUBLISHER_ID, item_id: itemId,
          item_title: item.item_title, item_tags: item.item_tags }),
      });
      console.log(`Uploaded ${item.producer_id} -> ${itemId}`);
    }

    const itemIds = Object.values(mapping);
    // Wait for every item to finish indexing before editing (see Part 1, Step 4).
  })();
  ```

  ```php PHP theme={null}
  <?php

  $clientId = 'your_client_id';
  $clientSecret = 'your_client_secret';
  $publisherId = 'your_publisher_id';

  $seedItems = [
      ['file' => 'volunteer-onboarding.md', 'producer_id' => 'rag-pipeline-part2-volunteer-onboarding',
       'item_title' => 'Onboarding New Volunteers', 'item_tags' => ['volunteers', 'rag-pipeline-series']],
      ['file' => 'measuring-community-impact.md', 'producer_id' => 'rag-pipeline-part2-measuring-impact',
       'item_title' => 'Measuring Community Impact', 'item_tags' => ['measurement', 'rag-pipeline-series']],
      ['file' => 'sustaining-engagement.md', 'producer_id' => 'rag-pipeline-part2-sustaining-engagement',
       'item_title' => 'Sustaining Long-Term Engagement', 'item_tags' => ['engagement', 'rag-pipeline-series']],
  ];

  // Get an access token (see the Authentication tutorial)
  $ch = curl_init('https://platform.ai.gloo.com/oauth2/token');
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_USERPWD => "$clientId:$clientSecret",
      CURLOPT_POSTFIELDS => http_build_query(['grant_type' => 'client_credentials', 'scope' => 'api/access']),
  ]);
  $token = json_decode(curl_exec($ch), true)['access_token'];
  curl_close($ch);

  // Upload each file and KEEP the returned item_id — your authoritative handle
  $mapping = [];
  foreach ($seedItems as $item) {
      $url = 'https://platform.ai.gloo.com/ingestion/v2/files?producer_id=' . urlencode($item['producer_id']);
      $ch = curl_init($url);
      curl_setopt_array($ch, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_POST => true,
          CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
          CURLOPT_POSTFIELDS => [
              'publisher_id' => $publisherId,
              'files' => new CURLFile("sample_files/{$item['file']}", 'text/markdown'),
          ],
      ]);
      $result = json_decode(curl_exec($ch), true);
      curl_close($ch);
      $itemId = ($result['ingesting'] ?: $result['duplicates'])[0];
      $mapping[$item['producer_id']] = $itemId;
      // Set initial metadata (see Part 1, Step 3)
      $ch = curl_init('https://platform.ai.gloo.com/engine/v2/item');
      curl_setopt_array($ch, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_CUSTOMREQUEST => 'PATCH',
          CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
          CURLOPT_POSTFIELDS => json_encode(['publisher_id' => $publisherId, 'item_id' => $itemId,
              'item_title' => $item['item_title'], 'item_tags' => $item['item_tags']]),
      ]);
      curl_exec($ch);
      curl_close($ch);
      echo "Uploaded {$item['producer_id']} -> $itemId\n";
  }

  $itemIds = array_values($mapping);
  // Wait for every item to finish indexing before editing (see Part 1, Step 4).
  ```

  ```go Go theme={null}
  // Seeding reuses Part 1's upload + polling. The key point: capture the item_id
  // the upload returns and keep it keyed by producer ID.
  mapping := map[string]string{}
  itemIDs := []string{}
  for _, item := range seedItems {
  	fileBytes, _ := os.ReadFile("sample_files/" + item.File)
  	var buf bytes.Buffer
  	writer := multipart.NewWriter(&buf)
  	writer.WriteField("publisher_id", publisherID)
  	part, _ := writer.CreateFormFile("files", item.File)
  	part.Write(fileBytes)
  	writer.Close()

  	uploadURL := "https://platform.ai.gloo.com/ingestion/v2/files?producer_id=" +
  		url.QueryEscape(item.ProducerID)
  	req, _ := http.NewRequest("POST", uploadURL, &buf)
  	req.Header.Set("Authorization", "Bearer "+token)
  	req.Header.Set("Content-Type", writer.FormDataContentType())
  	resp, _ := http.DefaultClient.Do(req)
  	var result struct {
  		Ingesting  []string `json:"ingesting"`
  		Duplicates []string `json:"duplicates"`
  	}
  	json.NewDecoder(resp.Body).Decode(&result)
  	resp.Body.Close()

  	itemID := append(result.Ingesting, result.Duplicates...)[0]
  	mapping[item.ProducerID] = itemID
  	itemIDs = append(itemIDs, itemID)
  	// Set initial metadata (see Part 1, Step 3), then wait for indexing (Part 1, Step 4).
  	fmt.Printf("Uploaded %s -> %s\n", item.ProducerID, itemID)
  }
  ```

  ```java Java theme={null}
  // Seeding reuses Part 1's upload + polling. The key point: capture the item_id
  // the upload returns and keep it keyed by producer ID.
  Map<String, String> mapping = new LinkedHashMap<>();
  List<String> itemIds = new ArrayList<>();
  for (SeedItem item : seedItems) {
      String boundary = "----GlooBoundary" + java.util.UUID.randomUUID();
      var body = new java.io.ByteArrayOutputStream();
      body.write(("--" + boundary + "\r\n"
          + "Content-Disposition: form-data; name=\"publisher_id\"\r\n\r\n"
          + PUBLISHER_ID + "\r\n").getBytes(java.nio.charset.StandardCharsets.UTF_8));
      body.write(("--" + boundary + "\r\n"
          + "Content-Disposition: form-data; name=\"files\"; filename=\"" + item.file() + "\"\r\n"
          + "Content-Type: text/markdown\r\n\r\n").getBytes(java.nio.charset.StandardCharsets.UTF_8));
      body.write(java.nio.file.Files.readAllBytes(java.nio.file.Path.of("sample_files", item.file())));
      body.write(("\r\n--" + boundary + "--\r\n").getBytes(java.nio.charset.StandardCharsets.UTF_8));

      HttpRequest uploadRequest = HttpRequest.newBuilder()
          .uri(URI.create("https://platform.ai.gloo.com/ingestion/v2/files?producer_id="
              + URLEncoder.encode(item.producerId(), StandardCharsets.UTF_8)))
          .header("Authorization", "Bearer " + token)
          .header("Content-Type", "multipart/form-data; boundary=" + boundary)
          .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
          .build();
      JsonObject result = gson.fromJson(
          http.send(uploadRequest, HttpResponse.BodyHandlers.ofString()).body(), JsonObject.class);
      JsonArray ingesting = result.getAsJsonArray("ingesting");
      String itemId = (ingesting.size() > 0 ? ingesting : result.getAsJsonArray("duplicates"))
          .get(0).getAsString();
      mapping.put(item.producerId(), itemId);
      itemIds.add(itemId);
      // Set initial metadata (see Part 1, Step 3), then wait for indexing (Part 1, Step 4).
      System.out.printf("Uploaded %s -> %s%n", item.producerId(), itemId);
  }
  ```
</CodeGroup>

### What You'll See

```
Uploaded rag-pipeline-part2-volunteer-onboarding -> 0bfc3629-137d-4797-aac6-a2836ca39930
Uploaded rag-pipeline-part2-measuring-impact -> 8e9f12bc-4ef2-4e77-b1aa-293935a67c9a
Uploaded rag-pipeline-part2-sustaining-engagement -> ca849be9-cf4c-4423-9369-3793736cd75a
```

<Note>
  Ingestion is asynchronous — wait until every item reaches `COMPLETED` (the polling loop from [Part 1, Step 4](/tutorials/rag-pipeline-part-1)) before editing it. The complete cookbook program includes this wait.
</Note>

The following steps reuse `token`, `PUBLISHER_ID`, `mapping`, and `item_ids` from here.

## Step 2: Update a Single Item

Correct one item's metadata with **PATCH** `/engine/v2/item`. Identify the target by `item_id` (or by `producer_id` — either works for a single item). Only the fields you send change; everything else is left as-is.

<CodeGroup>
  ```python Python theme={null}
  target_id = mapping["rag-pipeline-part2-volunteer-onboarding"]
  response = requests.patch(
      "https://platform.ai.gloo.com/engine/v2/item",
      headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
      json={
          "publisher_id": PUBLISHER_ID,
          "item_id": target_id,
          "item_title": "Onboarding New Volunteers: A First-Day Playbook",
          "item_summary": "A practical first-day checklist for welcoming and retaining new volunteers.",
      },
  )
  response.raise_for_status()
  print("Single item updated")
  ```

  ```javascript JavaScript theme={null}
  const targetId = mapping["rag-pipeline-part2-volunteer-onboarding"];
  const updateResponse = await fetch("https://platform.ai.gloo.com/engine/v2/item", {
    method: "PATCH",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      publisher_id: PUBLISHER_ID,
      item_id: targetId,
      item_title: "Onboarding New Volunteers: A First-Day Playbook",
      item_summary: "A practical first-day checklist for welcoming and retaining new volunteers.",
    }),
  });
  if (!updateResponse.ok) throw new Error(`HTTP ${updateResponse.status}`);
  console.log("Single item updated");
  ```

  ```typescript TypeScript theme={null}
  const targetId = mapping["rag-pipeline-part2-volunteer-onboarding"];
  const updateResponse = await fetch("https://platform.ai.gloo.com/engine/v2/item", {
    method: "PATCH",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      publisher_id: PUBLISHER_ID,
      item_id: targetId,
      item_title: "Onboarding New Volunteers: A First-Day Playbook",
      item_summary: "A practical first-day checklist for welcoming and retaining new volunteers.",
    }),
  });
  if (!updateResponse.ok) throw new Error(`HTTP ${updateResponse.status}`);
  console.log("Single item updated");
  ```

  ```php PHP theme={null}
  $targetId = $mapping['rag-pipeline-part2-volunteer-onboarding'];
  $ch = curl_init('https://platform.ai.gloo.com/engine/v2/item');
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_CUSTOMREQUEST => 'PATCH',
      CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
      CURLOPT_POSTFIELDS => json_encode([
          'publisher_id' => $publisherId,
          'item_id' => $targetId,
          'item_title' => 'Onboarding New Volunteers: A First-Day Playbook',
          'item_summary' => 'A practical first-day checklist for welcoming and retaining new volunteers.',
      ]),
  ]);
  curl_exec($ch);
  curl_close($ch);
  echo "Single item updated\n";
  ```

  ```go Go theme={null}
  targetID := mapping["rag-pipeline-part2-volunteer-onboarding"]
  update, _ := json.Marshal(map[string]any{
  	"publisher_id": publisherID,
  	"item_id":      targetID,
  	"item_title":   "Onboarding New Volunteers: A First-Day Playbook",
  	"item_summary": "A practical first-day checklist for welcoming and retaining new volunteers.",
  })
  req, _ = http.NewRequest("PATCH", "https://platform.ai.gloo.com/engine/v2/item",
  	bytes.NewReader(update))
  req.Header.Set("Authorization", "Bearer "+token)
  req.Header.Set("Content-Type", "application/json")
  resp, _ = http.DefaultClient.Do(req)
  resp.Body.Close()
  fmt.Println("Single item updated")
  ```

  ```java Java theme={null}
  String targetId = mapping.get("rag-pipeline-part2-volunteer-onboarding");
  String updateJson = """
      {
        "publisher_id": "%s",
        "item_id": "%s",
        "item_title": "Onboarding New Volunteers: A First-Day Playbook",
        "item_summary": "A practical first-day checklist for welcoming and retaining new volunteers."
      }
      """.formatted(PUBLISHER_ID, targetId);
  HttpRequest updateRequest = HttpRequest.newBuilder()
      .uri(URI.create("https://platform.ai.gloo.com/engine/v2/item"))
      .header("Authorization", "Bearer " + token)
      .header("Content-Type", "application/json")
      .method("PATCH", HttpRequest.BodyPublishers.ofString(updateJson))
      .build();
  http.send(updateRequest, HttpResponse.BodyHandlers.ofString());
  System.out.println("Single item updated");
  ```
</CodeGroup>

## Step 3: Bulk-Edit Multiple Items

Apply changes to several items at once with **PATCH** `/engine/v2/items?publisher_id=...`. The request has two parts: a `filter` selecting which items to touch, and a list of `ops` describing the changes. Here the filter is the exact `item_ids` you captured in Step 1 — that's what keeps the operation scoped to your content.

Each op has an `op` (`append`, `replace`, or `remove`), a `field`, and a `value`. Below, every item gets a `reviewed-q2-2026` tag appended and its author replaced.

<CodeGroup>
  ```python Python theme={null}
  response = requests.patch(
      "https://platform.ai.gloo.com/engine/v2/items",
      headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
      params={"publisher_id": PUBLISHER_ID},
      json={
          "filter": {"item_ids": item_ids},
          "ops": [
              {"op": "append", "field": "item_tags", "value": ["reviewed-q2-2026"]},
              {"op": "replace", "field": "author", "value": ["Community Programs Team"]},
          ],
      },
  )
  response.raise_for_status()
  result = response.json()
  print(f"Matched {result['total_matched']}, patched {result['total_patched']}, "
        f"failed {result['total_failed']}")
  ```

  ```javascript JavaScript theme={null}
  const bulkResponse = await fetch(
    `https://platform.ai.gloo.com/engine/v2/items?publisher_id=${encodeURIComponent(PUBLISHER_ID)}`,
    {
      method: "PATCH",
      headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        filter: { item_ids: itemIds },
        ops: [
          { op: "append", field: "item_tags", value: ["reviewed-q2-2026"] },
          { op: "replace", field: "author", value: ["Community Programs Team"] },
        ],
      }),
    }
  );
  if (!bulkResponse.ok) throw new Error(`HTTP ${bulkResponse.status}`);
  const result = await bulkResponse.json();
  console.log(
    `Matched ${result.total_matched}, patched ${result.total_patched}, failed ${result.total_failed}`
  );
  ```

  ```typescript TypeScript theme={null}
  const bulkResponse = await fetch(
    `https://platform.ai.gloo.com/engine/v2/items?publisher_id=${encodeURIComponent(PUBLISHER_ID)}`,
    {
      method: "PATCH",
      headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        filter: { item_ids: itemIds },
        ops: [
          { op: "append", field: "item_tags", value: ["reviewed-q2-2026"] },
          { op: "replace", field: "author", value: ["Community Programs Team"] },
        ],
      }),
    }
  );
  if (!bulkResponse.ok) throw new Error(`HTTP ${bulkResponse.status}`);
  const result = (await bulkResponse.json()) as {
    total_matched: number;
    total_patched: number;
    total_failed: number;
  };
  console.log(
    `Matched ${result.total_matched}, patched ${result.total_patched}, failed ${result.total_failed}`
  );
  ```

  ```php PHP theme={null}
  $ch = curl_init('https://platform.ai.gloo.com/engine/v2/items?publisher_id=' . urlencode($publisherId));
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_CUSTOMREQUEST => 'PATCH',
      CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
      CURLOPT_POSTFIELDS => json_encode([
          'filter' => ['item_ids' => $itemIds],
          'ops' => [
              ['op' => 'append', 'field' => 'item_tags', 'value' => ['reviewed-q2-2026']],
              ['op' => 'replace', 'field' => 'author', 'value' => ['Community Programs Team']],
          ],
      ]),
  ]);
  $result = json_decode(curl_exec($ch), true);
  curl_close($ch);
  echo "Matched {$result['total_matched']}, patched {$result['total_patched']}, "
      . "failed {$result['total_failed']}\n";
  ```

  ```go Go theme={null}
  bulk, _ := json.Marshal(map[string]any{
  	"filter": map[string]any{"item_ids": itemIDs},
  	"ops": []map[string]any{
  		{"op": "append", "field": "item_tags", "value": []string{"reviewed-q2-2026"}},
  		{"op": "replace", "field": "author", "value": []string{"Community Programs Team"}},
  	},
  })
  bulkURL := "https://platform.ai.gloo.com/engine/v2/items?publisher_id=" + url.QueryEscape(publisherID)
  req, _ = http.NewRequest("PATCH", bulkURL, bytes.NewReader(bulk))
  req.Header.Set("Authorization", "Bearer "+token)
  req.Header.Set("Content-Type", "application/json")
  resp, _ = http.DefaultClient.Do(req)
  defer resp.Body.Close()
  var result struct {
  	TotalMatched int `json:"total_matched"`
  	TotalPatched int `json:"total_patched"`
  	TotalFailed  int `json:"total_failed"`
  }
  json.NewDecoder(resp.Body).Decode(&result)
  fmt.Printf("Matched %d, patched %d, failed %d\n",
  	result.TotalMatched, result.TotalPatched, result.TotalFailed)
  ```

  ```java Java theme={null}
  String bulkJson = """
      {
        "filter": { "item_ids": %s },
        "ops": [
          { "op": "append", "field": "item_tags", "value": ["reviewed-q2-2026"] },
          { "op": "replace", "field": "author", "value": ["Community Programs Team"] }
        ]
      }
      """.formatted(gson.toJson(itemIds));
  HttpRequest bulkRequest = HttpRequest.newBuilder()
      .uri(URI.create("https://platform.ai.gloo.com/engine/v2/items?publisher_id="
          + URLEncoder.encode(PUBLISHER_ID, StandardCharsets.UTF_8)))
      .header("Authorization", "Bearer " + token)
      .header("Content-Type", "application/json")
      .method("PATCH", HttpRequest.BodyPublishers.ofString(bulkJson))
      .build();
  JsonObject result = gson.fromJson(
      http.send(bulkRequest, HttpResponse.BodyHandlers.ofString()).body(), JsonObject.class);
  System.out.printf("Matched %d, patched %d, failed %d%n",
      result.get("total_matched").getAsInt(),
      result.get("total_patched").getAsInt(),
      result.get("total_failed").getAsInt());
  ```
</CodeGroup>

### What You'll See

```
Matched 3, patched 3, failed 0
```

## Step 4: Verify Your Changes

Re-fetch each item with **GET** `/engine/v2/items/{item_id}` to confirm the edits.

<Note>
  Edits succeed immediately, but the read path is **eventually consistent**: a freshly patched item can take a few seconds to reflect the change on a subsequent GET. Verify by re-fetching until the change appears, rather than reading once.
</Note>

<CodeGroup>
  ```python Python theme={null}
  import time

  def get_item(item_id):
      r = requests.get(
          f"https://platform.ai.gloo.com/engine/v2/items/{item_id}",
          headers={"Authorization": f"Bearer {token}"},
      )
      r.raise_for_status()
      return r.json()

  for item_id in item_ids:
      item = get_item(item_id)
      for _ in range(20):  # read-after-write retry
          if "reviewed-q2-2026" in (item.get("item_tags") or []):
              break
          time.sleep(3)
          item = get_item(item_id)
      print(item["item_title"])
      print(f"  author: {', '.join(item.get('author') or [])}")
      print(f"  tags:   {', '.join(item.get('item_tags') or [])}")
  ```

  ```javascript JavaScript theme={null}
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  async function getItem(itemId) {
    const r = await fetch(`https://platform.ai.gloo.com/engine/v2/items/${itemId}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return r.json();
  }

  for (const itemId of itemIds) {
    let item = await getItem(itemId);
    for (let attempt = 0; attempt < 20; attempt++) {
      // read-after-write retry
      if ((item.item_tags ?? []).includes("reviewed-q2-2026")) break;
      await sleep(3000);
      item = await getItem(itemId);
    }
    console.log(item.item_title);
    console.log(`  author: ${(item.author ?? []).join(", ")}`);
    console.log(`  tags:   ${(item.item_tags ?? []).join(", ")}`);
  }
  ```

  ```typescript TypeScript theme={null}
  const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));

  interface ItemMetadata {
    item_title: string;
    author: string[];
    item_tags: string[];
  }

  async function getItem(itemId: string): Promise<ItemMetadata> {
    const r = await fetch(`https://platform.ai.gloo.com/engine/v2/items/${itemId}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return (await r.json()) as ItemMetadata;
  }

  for (const itemId of itemIds) {
    let item = await getItem(itemId);
    for (let attempt = 0; attempt < 20; attempt++) {
      // read-after-write retry
      if ((item.item_tags ?? []).includes("reviewed-q2-2026")) break;
      await sleep(3000);
      item = await getItem(itemId);
    }
    console.log(item.item_title);
    console.log(`  author: ${(item.author ?? []).join(", ")}`);
    console.log(`  tags:   ${(item.item_tags ?? []).join(", ")}`);
  }
  ```

  ```php PHP theme={null}
  function getItem(string $itemId, string $token): array
  {
      $ch = curl_init("https://platform.ai.gloo.com/engine/v2/items/$itemId");
      curl_setopt_array($ch, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
      ]);
      $item = json_decode(curl_exec($ch), true);
      curl_close($ch);
      return $item;
  }

  foreach ($itemIds as $itemId) {
      $item = getItem($itemId, $token);
      for ($attempt = 0; $attempt < 20; $attempt++) { // read-after-write retry
          if (in_array('reviewed-q2-2026', $item['item_tags'] ?? [], true)) {
              break;
          }
          sleep(3);
          $item = getItem($itemId, $token);
      }
      echo "{$item['item_title']}\n";
      echo '  author: ' . implode(', ', $item['author'] ?? []) . "\n";
      echo '  tags:   ' . implode(', ', $item['item_tags'] ?? []) . "\n";
  }
  ```

  ```go Go theme={null}
  getItem := func(itemID string) map[string]any {
  	req, _ := http.NewRequest("GET",
  		"https://platform.ai.gloo.com/engine/v2/items/"+itemID, nil)
  	req.Header.Set("Authorization", "Bearer "+token)
  	resp, _ := http.DefaultClient.Do(req)
  	defer resp.Body.Close()
  	var item map[string]any
  	json.NewDecoder(resp.Body).Decode(&item)
  	return item
  }

  hasTag := func(item map[string]any, tag string) bool {
  	tags, _ := item["item_tags"].([]any)
  	for _, t := range tags {
  		if t == tag {
  			return true
  		}
  	}
  	return false
  }

  for _, itemID := range itemIDs {
  	item := getItem(itemID)
  	for attempt := 0; attempt < 20; attempt++ { // read-after-write retry
  		if hasTag(item, "reviewed-q2-2026") {
  			break
  		}
  		time.Sleep(3 * time.Second)
  		item = getItem(itemID)
  	}
  	fmt.Println(item["item_title"])
  	fmt.Println("  author:", item["author"])
  	fmt.Println("  tags:  ", item["item_tags"])
  }
  ```

  ```java Java theme={null}
  for (String itemId : mapping.values()) {
      JsonObject item = null;
      for (int attempt = 0; attempt < 20; attempt++) { // read-after-write retry
          HttpRequest getRequest = HttpRequest.newBuilder()
              .uri(URI.create("https://platform.ai.gloo.com/engine/v2/items/" + itemId))
              .header("Authorization", "Bearer " + token)
              .GET()
              .build();
          item = gson.fromJson(
              http.send(getRequest, HttpResponse.BodyHandlers.ofString()).body(), JsonObject.class);
          if (item.getAsJsonArray("item_tags").toString().contains("reviewed-q2-2026")) break;
          Thread.sleep(3000);
      }
      System.out.println(item.get("item_title").getAsString());
      System.out.println("  author: " + item.getAsJsonArray("author"));
      System.out.println("  tags:   " + item.getAsJsonArray("item_tags"));
  }
  ```
</CodeGroup>

### What You'll See

```
Onboarding New Volunteers: A First-Day Playbook
  author: Community Programs Team
  tags:   volunteers, rag-pipeline-series, reviewed-q2-2026
Measuring Community Impact
  author: Community Programs Team
  tags:   measurement, rag-pipeline-series, reviewed-q2-2026
Sustaining Long-Term Engagement
  author: Community Programs Team
  tags:   engagement, rag-pipeline-series, reviewed-q2-2026
```

The single-item update from Step 2 (the new title on the first item) and the bulk edits from Step 3 (new author and added tag on all three) are both reflected.

## Step 5: Delete Items

Remove items with **DELETE** `/engine/v2/items`, passing the `item_ids` to delete. This both demonstrates deletion and cleans up the content this recipe created.

<CodeGroup>
  ```python Python theme={null}
  response = requests.delete(
      "https://platform.ai.gloo.com/engine/v2/items",
      headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
      json={"item_ids": item_ids},
  )
  response.raise_for_status()
  deletion = response.json()
  print(f"Requested {deletion['total_requested']}, deleted {deletion['total_deleted']}, "
        f"failed {deletion['total_failed']}")

  # GET is authoritative for deletion: it returns 404 once an item is gone.
  for item_id in item_ids:
      r = requests.get(
          f"https://platform.ai.gloo.com/engine/v2/items/{item_id}",
          headers={"Authorization": f"Bearer {token}"},
      )
      print(f"{item_id} -> {r.status_code}")
  ```

  ```javascript JavaScript theme={null}
  const deleteResponse = await fetch("https://platform.ai.gloo.com/engine/v2/items", {
    method: "DELETE",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({ item_ids: itemIds }),
  });
  if (!deleteResponse.ok) throw new Error(`HTTP ${deleteResponse.status}`);
  const deletion = await deleteResponse.json();
  console.log(
    `Requested ${deletion.total_requested}, deleted ${deletion.total_deleted}, failed ${deletion.total_failed}`
  );

  // GET is authoritative for deletion: it returns 404 once an item is gone.
  for (const itemId of itemIds) {
    const r = await fetch(`https://platform.ai.gloo.com/engine/v2/items/${itemId}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    console.log(`${itemId} -> ${r.status}`);
  }
  ```

  ```typescript TypeScript theme={null}
  const deleteResponse = await fetch("https://platform.ai.gloo.com/engine/v2/items", {
    method: "DELETE",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({ item_ids: itemIds }),
  });
  if (!deleteResponse.ok) throw new Error(`HTTP ${deleteResponse.status}`);
  const deletion = (await deleteResponse.json()) as {
    total_requested: number;
    total_deleted: number;
    total_failed: number;
  };
  console.log(
    `Requested ${deletion.total_requested}, deleted ${deletion.total_deleted}, failed ${deletion.total_failed}`
  );

  // GET is authoritative for deletion: it returns 404 once an item is gone.
  for (const itemId of itemIds) {
    const r = await fetch(`https://platform.ai.gloo.com/engine/v2/items/${itemId}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    console.log(`${itemId} -> ${r.status}`);
  }
  ```

  ```php PHP theme={null}
  $ch = curl_init('https://platform.ai.gloo.com/engine/v2/items');
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_CUSTOMREQUEST => 'DELETE',
      CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
      CURLOPT_POSTFIELDS => json_encode(['item_ids' => $itemIds]),
  ]);
  $deletion = json_decode(curl_exec($ch), true);
  curl_close($ch);
  echo "Requested {$deletion['total_requested']}, deleted {$deletion['total_deleted']}, "
      . "failed {$deletion['total_failed']}\n";

  // GET is authoritative for deletion: it returns 404 once an item is gone.
  foreach ($itemIds as $itemId) {
      $ch = curl_init("https://platform.ai.gloo.com/engine/v2/items/$itemId");
      curl_setopt_array($ch, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
      ]);
      curl_exec($ch);
      $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
      curl_close($ch);
      echo "$itemId -> $status\n";
  }
  ```

  ```go Go theme={null}
  del, _ := json.Marshal(map[string][]string{"item_ids": itemIDs})
  req, _ = http.NewRequest("DELETE", "https://platform.ai.gloo.com/engine/v2/items",
  	bytes.NewReader(del))
  req.Header.Set("Authorization", "Bearer "+token)
  req.Header.Set("Content-Type", "application/json")
  resp, _ = http.DefaultClient.Do(req)
  var deletion struct {
  	TotalRequested int `json:"total_requested"`
  	TotalDeleted   int `json:"total_deleted"`
  	TotalFailed    int `json:"total_failed"`
  }
  json.NewDecoder(resp.Body).Decode(&deletion)
  resp.Body.Close()
  fmt.Printf("Requested %d, deleted %d, failed %d\n",
  	deletion.TotalRequested, deletion.TotalDeleted, deletion.TotalFailed)

  // GET is authoritative for deletion: it returns 404 once an item is gone.
  for _, itemID := range itemIDs {
  	req, _ := http.NewRequest("GET",
  		"https://platform.ai.gloo.com/engine/v2/items/"+itemID, nil)
  	req.Header.Set("Authorization", "Bearer "+token)
  	resp, _ := http.DefaultClient.Do(req)
  	resp.Body.Close()
  	fmt.Printf("%s -> %d\n", itemID, resp.StatusCode)
  }
  ```

  ```java Java theme={null}
  HttpRequest deleteRequest = HttpRequest.newBuilder()
      .uri(URI.create("https://platform.ai.gloo.com/engine/v2/items"))
      .header("Authorization", "Bearer " + token)
      .header("Content-Type", "application/json")
      .method("DELETE", HttpRequest.BodyPublishers.ofString(
          gson.toJson(Map.of("item_ids", itemIds))))
      .build();
  JsonObject deletion = gson.fromJson(
      http.send(deleteRequest, HttpResponse.BodyHandlers.ofString()).body(), JsonObject.class);
  System.out.printf("Requested %d, deleted %d, failed %d%n",
      deletion.get("total_requested").getAsInt(),
      deletion.get("total_deleted").getAsInt(),
      deletion.get("total_failed").getAsInt());

  // GET is authoritative for deletion: it returns 404 once an item is gone.
  for (String itemId : itemIds) {
      HttpRequest getRequest = HttpRequest.newBuilder()
          .uri(URI.create("https://platform.ai.gloo.com/engine/v2/items/" + itemId))
          .header("Authorization", "Bearer " + token)
          .GET()
          .build();
      int status = http.send(getRequest, HttpResponse.BodyHandlers.ofString()).statusCode();
      System.out.printf("%s -> %d%n", itemId, status);
  }
  ```
</CodeGroup>

### What You'll See

```
Requested 3, deleted 3, failed 0
0bfc3629-137d-4797-aac6-a2836ca39930 -> 404
8e9f12bc-4ef2-4e77-b1aa-293935a67c9a -> 404
ca849be9-cf4c-4423-9369-3793736cd75a -> 404
```

<Warning>
  A `GET` on the item ID is authoritative for deletion — it returns 404 as soon as the item is gone. Don't use the `by-producer` lookup to confirm a delete: it's cached and can keep returning the deleted item's ID for a few minutes.
</Warning>

## Run the Complete Example

The cookbook runs the whole lifecycle — seed, update, bulk-edit, verify, and delete — as one program in all six languages. From the [cookbook repository](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook), install dependencies, copy `.env.example` to `.env` and add your credentials, then run it:

<CodeGroup>
  ```bash Python theme={null}
  cd rag-pipeline-part-2/python
  python3 -m venv venv
  source venv/bin/activate  # On Windows: venv\Scripts\activate
  pip install -r requirements.txt
  cp .env.example .env       # then add your Client ID, Secret, and Publisher ID
  python main.py
  ```

  ```bash JavaScript theme={null}
  cd rag-pipeline-part-2/javascript
  npm install
  cp .env.example .env       # then add your Client ID, Secret, and Publisher ID
  npm start
  ```

  ```bash TypeScript theme={null}
  cd rag-pipeline-part-2/typescript
  npm install
  cp .env.example .env       # then add your Client ID, Secret, and Publisher ID
  npm start
  ```

  ```bash PHP theme={null}
  cd rag-pipeline-part-2/php
  composer install
  cp .env.example .env       # then add your Client ID, Secret, and Publisher ID
  php index.php
  ```

  ```bash Go theme={null}
  cd rag-pipeline-part-2/go
  go mod tidy
  cp .env.example .env       # then add your Client ID, Secret, and Publisher ID
  go run main.go
  ```

  ```bash Java theme={null}
  cd rag-pipeline-part-2/java
  mvn compile
  cp .env.example .env       # then add your Client ID, Secret, and Publisher ID
  mvn -q exec:java
  ```
</CodeGroup>

You'll see:

```
Step 1: Seeding sample content...
  Uploaded rag-pipeline-part2-volunteer-onboarding -> 0bfc3629-137d-4797-aac6-a2836ca39930
  Uploaded rag-pipeline-part2-measuring-impact -> 8e9f12bc-4ef2-4e77-b1aa-293935a67c9a
  Uploaded rag-pipeline-part2-sustaining-engagement -> ca849be9-cf4c-4423-9369-3793736cd75a
  Waiting for ingestion to complete (this can take a few minutes)...
  All seed items indexed.

Step 2: Updating a single item...
  Updated title and summary for rag-pipeline-part2-volunteer-onboarding

Step 3: Bulk-editing all seeded items...
  Matched 3, patched 3, failed 0

Step 4: Verifying changes...
  Onboarding New Volunteers: A First-Day Playbook
    author: Community Programs Team
    tags:   volunteers, rag-pipeline-series, reviewed-q2-2026
  Measuring Community Impact
    author: Community Programs Team
    tags:   measurement, rag-pipeline-series, reviewed-q2-2026
  Sustaining Long-Term Engagement
    author: Community Programs Team
    tags:   engagement, rag-pipeline-series, reviewed-q2-2026

Step 5: Deleting items (cleanup)...
  Requested 3, deleted 3, failed 0
  All items confirmed deleted: true

Lifecycle complete. The publisher is back to its pre-recipe state.
```

## Working Code Sample

<Card title="View Complete Code" icon="github" href="https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/rag-pipeline-part-2">
  Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions and the sample content files.
</Card>

<Tip>
  The code snippets above are **simplified and self-contained** — designed for readability and easy copy-paste. The cookbook examples add token caching, the indexing wait, and structured error handling. Both implement the same APIs and patterns.
</Tip>

## Troubleshooting

### Error: 400 Invalid request — Item ID or Producer ID with Publisher ID required

A single-item `PATCH /engine/v2/item` needs `publisher_id` plus either `item_id` or `producer_id`. Include one of the identifiers.

### Bulk patch reports fewer matched than expected

The `filter` didn't match all your items. Confirm your `item_ids` are the ones returned by the uploads and that the items still exist (a prior run may have deleted them).

### Verification shows stale metadata

Reads are eventually consistent. Re-fetch until the change appears (as shown in Step 4) rather than reading once immediately after an edit.

### Editing right after upload returns 404 item\_not\_found

If you looked the item up with `by-producer`, its result may be stale (it's cached \~5 minutes and not invalidated on writes). Use the `item_id` from the upload response instead — that's authoritative.

## Next Steps

You can now manage content through its full lifecycle, safely scoped to the items you own. To round out the pipeline:

1. **[Part 3: Verification, Error Handling & Resilience](/tutorials/rag-pipeline-part-3)** — interpret API error responses and add retry patterns for production
2. **[Building Custom Search](/tutorials/search)** — surface your updated content to users
3. **[Grounded Completions with RAG](/tutorials/completions-grounded)** — answer questions from your content with citations
