{
  "schema_version": 2,
  "id": "develop/ai/context-engine/agent-playbooks/proposals",
  "title": "Proposals",
  "url": "https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/proposals/",
  "summary": "Turn suggested answers from agents and applications into reviewed FAQs for your playbook.",
  "tags": [
    "docs",
    "develop",
    "ai"
  ],
  "last_updated": "2026-11-02T15:54:35-08:00",
  "page_type": "content",
  "content_hash": "64d725428deb2a6e5394b4cb87ce789273bda14de90a15a224ac6a9abea20e97",
  "sections": [
    {
      "id": "code-examples",
      "title": "Code examples",
      "role": "example",
      "text": "These examples connect to Iris services using SDKs or HTTP requests with curl. Follow one language throughout each walkthrough.\n\n\nUse proposals to review suggested frequently asked questions (FAQs) and skills before publishing them. An agent or application submits candidate content; a reviewer checks its content, resources, and evidence before acceptance creates an unpublished entry.\n\nProposals create new entries. They do not revise existing entries. To correct existing content, use [FAQ versioning](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/manage-faqs#update-an-answer) or [skill versioning](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/manage-skills#update-a-skill).\n\nStart with the FAQ walkthrough or [review a skill proposal](#review-skill-proposals). [Automatic extraction](#automate-proposal-generation) from Redis Agent Memory suggests FAQs only. Neither path affects agent responses until you publish the accepted entry.\n\n![A proposal enters the inbox with candidate content and evidence. Acceptance creates an unpublished FAQ, which publication serves to agents. Dismissal or inbox expiration moves the proposal to trash, where it can be restored before its retention deadline.](https://redis.io/docs/latest/images/ai/context-engine/agent-playbook-proposals.svg)"
    },
    {
      "id": "before-you-begin",
      "title": "Before you begin",
      "role": "content",
      "text": "Use an existing playbook and application programming interface (API) keys with these permissions:\n\n- `propose` to create candidates and attach evidence.\n- `write` to edit, accept, dismiss, restore, or purge proposals.\n- `publish` to publish the entry returned by acceptance.\n\nFor this walkthrough, give `IRIS_AGENT_PLAYBOOKS_API_KEY` all three permissions and follow the quickstart's [client setup](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/quickstart#connect-to-the-playbook). Append the snippets in order and run each write once. In your application, you can separate suggestion, review, and publication across keys.\n\nFor the skill examples and complete Python or cURL program, first [prepare the sample skill package](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/manage-skills#prepare-a-skill-package). The pinned CLI's proposal commands support FAQs; use the SDK or cURL for skill proposals."
    },
    {
      "id": "find-proposals-to-review",
      "title": "Find proposals to review",
      "role": "content",
      "text": "A key with `read` permission can list, search, and inspect proposals. The inbox contains candidates awaiting review and any acceptance still in progress. Only open candidates can be edited.\n\nFor software development kit (SDK) and cURL lists, follow the cursor until it is absent, even after an empty page. Keep the view and sort settings unchanged. Responses return `items` and `nextCursor` and don't include a total count. The command-line interface (CLI) follows cursors automatically. Use `--page` for one page with `items` and `nextCursor`, then `--cursor` to continue.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]\n\n\n\nSearch before submitting another candidate. Compare similar questions to decide whether they need the same answer. Proposal search covers open candidates, not trash, accepted proposals, or [published FAQs](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/retrieval#search).\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]"
    },
    {
      "id": "create-a-proposal",
      "title": "Create a proposal",
      "role": "content",
      "text": "Write a self-contained question and answer. Add intents for the task and attributes for its audience, then call `create_proposal` with an `add` action targeting `newEntry`. You can supply an entry key for the resulting FAQ and immutable source context that a reviewer can check.\n\nSave the proposal identifier and `revision`. Use the revision to identify the candidate you edit or accept.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]\n\n\n\nThe CLI takes a flat FAQ candidate file and a separate source file in JavaScript Object Notation (JSON), and constructs the proposal action for you."
    },
    {
      "id": "handle-an-existing-candidate",
      "title": "Handle an existing candidate",
      "role": "content",
      "text": "If creation or replacement returns `409` with type `/errors/proposal-already-exists`, read the existing proposal. The error body names it in `existingId`; Python exposes it as `error.data.existing_id` on `ConflictErrorResponseContent`.\n\nIf the open candidate addresses the same need, [attach evidence](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/proposals#attach-evidence) to it rather than creating another suggestion. Review its current revision before editing or accepting it."
    },
    {
      "id": "attach-evidence",
      "title": "Attach evidence",
      "role": "content",
      "text": "Use `submit_proposal_evidence` to add another observation to an open proposal. For example, attach a policy excerpt that supports the answer or a conversation that reveals a missing condition.\n\n| HTTP/CLI field | Python argument or dictionary field | Purpose |\n|:---------------|:------------------------------------|:--------|\n| `evidenceId` | `evidence_id` | Required observation identifier: `evd_` followed by 32 lowercase hexadecimal characters. Reuse it for an exact retry. |\n| `observedAt` | `observed_at` | Required time of the source observation. |\n| `producer.name` | `producer[\"name\"]` | Required application or agent name. |\n| `producer.promptVersion` | `producer[\"prompt_version\"]` | Optional prompt version. |\n| `producer.commitSHA` | `producer[\"commit_sha\"]` | Optional commit hash: 40 or 64 lowercase hexadecimal characters. |\n| `source.kind` | `source[\"kind\"]` | Required source label. |\n| `source.reference` | `source[\"reference\"]` | Optional JSON object locating the source. |\n| `source.content` | `source[\"content\"]` | Optional JSON object containing the observation or excerpt. |\n\nInclude a supporting passage and a reference the reviewer can check. Redis Agent Playbooks do not independently verify caller-supplied attribution. An evidence count measures observations, not correctness or independent users.\n\nEvidence is immutable. Retrying the same identifier and payload returns the disposition `ALREADY_PRESENT`; new evidence returns `ADDED`. Reusing that identifier for changed content returns a conflict. Use a new identifier for a new observation.\n\nThe creation source explains the first suggestion and does not increase `evidenceCount`. Attached evidence stays with the proposal; acceptance does not copy it into the FAQ. Check [evidence limits](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/limits#proposal-and-evidence-limits) before attaching large passages.\n\nThis example attaches a policy excerpt. The cURL and CLI tabs use `openssl` to generate an identifier.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]"
    },
    {
      "id": "review-and-edit-a-candidate",
      "title": "Review and edit a candidate",
      "role": "content",
      "text": "1. Retrieve the complete proposal and its evidence with `get_proposal`.\n1. Check that the sources support the answer, its conditions, and its intended audience.\n1. Check for sensitive or conversation-specific details that should not become reusable guidance.\n1. Replace the candidate with `replace_proposal` if it needs changes. Supply the complete action and `expected_revision` for the revision you read (`expectedRevision` in HTTP).\n1. Review the returned candidate and record its new revision.\n\nEditing preserves evidence. If another reviewer changes the candidate, the revision check fails; review the current answer before trying again.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]\n\n\n\nAdd the excerpt's guidance about card details. Preserve the other fields from the proposal you just read.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]"
    },
    {
      "id": "dismiss-restore-or-purge",
      "title": "Dismiss, restore, or purge",
      "role": "content",
      "text": "Dismiss a candidate you do not want to accept. You can inspect or restore it from trash before its retention deadline. Restore it before editing, attaching evidence, or accepting it.\n\nOpen proposals also expire. Check `inboxExpiresAt` and `purgeAt`, and see [review and retention limits](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/limits#proposal-and-evidence-limits) for how activity changes those deadlines.\n\nPurge a trashed proposal to remove it permanently. Save any review records your application needs first.\n\nTest dismissal and restoration before accepting the sample. Purging instead ends the walkthrough for that proposal.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]"
    },
    {
      "id": "accept-and-publish",
      "title": "Accept and publish",
      "role": "content",
      "text": "Call `accept_proposal` with the revision you reviewed. Acceptance creates one unpublished FAQ. Use the entry identifier and version in `result.versionCreated` to read that FAQ, then [publish the reviewed version](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/manage-faqs#publish-a-version) with a key that has `publish` permission.\n\nA proposal being accepted (state `PROMOTING`) or already accepted (`PROMOTED`) cannot be edited. If acceptance is interrupted, retry it rather than creating another FAQ. The returned receipt is historical: it does not contain the answer or confirm that the FAQ still exists. Inspect the FAQ before acting on a retried result.\n\nIf your client cannot recognize the result type, inspect the result or upgrade the client before publishing.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]\n\n\n\nAccept the reviewed revision.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]\n\n\n\nPublish the returned FAQ version after review.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]\n\n\n\nThe accepted FAQ remains after this example. Delete it with the [FAQ cleanup procedure](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/manage-faqs#withdraw-or-delete-an-faq) using the returned entry identifier when you finish."
    },
    {
      "id": "inspect-promotion-history",
      "title": "Inspect promotion history",
      "role": "content",
      "text": "Use the trash view to inspect dismissed, expired, and accepted proposals while they are retained. Accepted proposals cannot be restored or edited. Purging their review record does not delete the FAQ. An acceptance still in progress remains in the inbox and cannot be purged.\n\n**Available in:** Python (Iris), cURL (Iris), CLI (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n##### CLI (Iris)\n\n[code example]"
    },
    {
      "id": "review-skill-proposals",
      "title": "Review skill proposals",
      "role": "content",
      "text": "Submit the complete skill ZIP in `skillPackage.packageBase64` under `content.payload`. The service validates the archive and returns normalized instructions and resource descriptors. A proposal resource has no published skill URI; read it through the proposal resource operation with the revision you reviewed.\n\nIf creation rejects `skillPackage` with `/errors/unknown-request-member`, ask your administrator to check that the service supports your SDK's API contract.\n\n**Available in:** Python (Iris), cURL (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n\n\nRead the candidate, inspect its instructions, and read the supporting files. This example verifies the reply template's SHA-256 digest before displaying its text. Do not run scripts merely because archive validation succeeded.\n\n**Available in:** Python (Iris), cURL (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n\n\nResource reads require `expectedRevision` (`expected_revision` in Python). If another reviewer replaces the candidate, a stale resource read or acceptance returns `412` with type `/errors/revision-conflict`. Read and review the new candidate and its files before retrying. To replace a skill candidate, submit a complete ZIP with the retained intents, attributes, and metadata; the normalized response payload is not an upload request.\n\nAccept the reviewed revision and check the receipt's `entryKind` before selecting a publication operation. A skill receipt identifies an unpublished skill version:\n\n**Available in:** Python (Iris), cURL (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n\n\nPublish the reviewed skill version with `publish_skill`:\n\n**Available in:** Python (Iris), cURL (Iris)\n\n##### Python (Iris)\n\n[code example]\n\n##### cURL (Iris)\n\n[code example]\n\n\n\nThe accepted skill remains published. Use its returned entry identifier with the [skill cleanup procedure](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/manage-skills#withdraw-or-delete-a-skill) when you finish. Purging a proposal's review record does not delete its accepted entry."
    },
    {
      "id": "automate-proposal-generation",
      "title": "Automate proposal generation",
      "role": "content",
      "text": "Choose how your application contributes suggestions:\n\n- **Redis Agent Memory:** Derive reusable answers from recorded agent conversations through background extraction.\n- **Proposal and evidence APIs:** Generate suggestions from your own sources and triggers, such as resolved support cases or evaluation findings.\n\nBoth paths feed the same review inbox. Background extraction generates FAQs; your application can submit FAQ or skill proposals. Automatic skill extraction is not supported."
    },
    {
      "id": "extract-faqs-from-agent-usage-in-redis-agent-memory",
      "title": "Extract FAQs from agent usage in Redis Agent Memory",
      "role": "content",
      "text": "1. [Record session events in Redis Agent Memory](https://redis.io/docs/latest/develop/ai/context-engine/agent-memory/developer-guide#build-context-for-each-agent-turn), including user questions and agent responses under the correct conversation identity. Retrieving playbook knowledge alone does not record an interaction.\n1. Ask your administrator to [configure and enable extraction](https://redis.io/docs/latest/operate/iris/agent-playbook/manage-playbook#automatic-faq-extraction) for the source store and destination playbook. Proposal permissions alone do not enable it.\n1. Let a representative conversation become idle, then [inspect the inbox](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/proposals#find-proposals-to-review) for suggestions and new evidence.\n\nEach playbook can extract from one whole Redis Agent Memory store in the same tenant; selecting individual namespaces is not supported. Keep source conversations available for extraction and review. Later events in a completed conversation do not automatically trigger another extraction.\n\nNot every conversation produces a proposal. It might still be active, contain no reusable answer, or support an existing FAQ or open candidate. Extraction can add evidence to an open proposal without replacing its answer. It never updates a published FAQ. See [extraction setup](https://redis.io/docs/latest/operate/iris/agent-playbook/manage-playbook#automatic-faq-extraction) and [operator troubleshooting](https://redis.io/docs/latest/operate/iris/agent-playbook/manage-playbook#troubleshoot-a-playbook) for timing and diagnosis."
    },
    {
      "id": "review-background-extraction-results",
      "title": "Review background extraction results",
      "role": "content",
      "text": "Open the proposal's source conversation or supporting quotes and check the generated answer against them. Extraction suggests reusable wording, which can differ from the conversation or reflect conflicting observations. Resolve those differences and remove conversation-specific details before following the [review and publication steps](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/proposals#review-and-edit-a-candidate)."
    },
    {
      "id": "generate-proposals-with-the-proposal-and-evidence-apis",
      "title": "Generate proposals with the proposal and evidence APIs",
      "role": "content",
      "text": "Use the Python, cURL, or CLI examples in your application when you need custom sources or generation rules. Redis Agent Memory is optional for this path. Your application selects the source material and generates the suggested answer.\n\n1. Extract a reusable question and answer from the source material, keeping a reference the reviewer can inspect.\n1. Compare it with [published FAQs](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/retrieval#search) and [open proposals](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/proposals#find-proposals-to-review). Check the question, conditions, and audience, rather than relying on similarity scores alone.\n1. [Attach evidence](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/proposals#attach-evidence) if an open candidate addresses the same need. A differing answer can help the reviewer; evidence does not edit the candidate.\n1. [Create a proposal](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/proposals#create-a-proposal) if the answer covers a new need. Save its identifier and revision.\n1. Route the candidate to a reviewer before accepting and publishing it.\n\nUse a key with `propose` permission, which includes `read`. Keep review and publication permissions separate. Put source identifiers in `source.reference` and supporting material in `source.content`, using JSON objects appropriate to your source.\n\n#### Handle retries and concurrent observations\n\nPersist evidence identifiers and their original payloads before submission so a background job can follow the [exact-retry rules](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/proposals#attach-evidence). Do not generate another identifier on each retry.\n\nProposal creation is not idempotent: after an uncertain response, inspect the inbox before creating again. Follow [existing-candidate handling](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/proposals#handle-an-existing-candidate) for duplicate errors. Concurrent submissions can still produce equivalent candidates that need review.\n\nRecheck the proposal if its state changes, and leave restoration or edits to an authorized reviewer. Check [proposal and evidence limits](https://redis.io/docs/latest/develop/ai/context-engine/agent-playbooks/limits#proposal-and-evidence-limits) and retain failed observations for investigation instead of retrying permanent validation errors."
    }
  ],
  "examples": [
    {
      "id": "find-proposals-to-review-ex0",
      "language": "python",
      "code": "cursor = None\nwhile True:\n    page = playbook.client.list_proposals(\n        view=\"INBOX\", sort_by=\"updatedAt\", sort_order=\"DESC\", limit=50, cursor=cursor\n    )\n    for item in page.items:\n        print(item.proposal_id, item.state, item.evidence_count, item.action)\n    cursor = page.next_cursor\n    if cursor is None:\n        break",
      "section_id": "find-proposals-to-review"
    },
    {
      "id": "find-proposals-to-review-ex1",
      "language": "bash",
      "code": "CURSOR=''\nwhile true; do\n  PAGE_ARGS=(--data-urlencode view=INBOX --data-urlencode sortBy=updatedAt --data-urlencode sortOrder=DESC --data-urlencode limit=50)\n  if [[ -n \"$CURSOR\" ]]; then PAGE_ARGS+=(--data-urlencode \"cursor=$CURSOR\"); fi\n  curl --fail-with-body --silent --show-error --get \"$PLAYBOOK_API/proposals\" \\\n    -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \"${PAGE_ARGS[@]}\" > inbox.json\n  jq '.items[] | {proposalId, state, evidenceCount, action}' inbox.json\n  CURSOR=$(jq -r '.nextCursor // empty' inbox.json)\n  [[ -n \"$CURSOR\" ]] || break\ndone",
      "section_id": "find-proposals-to-review"
    },
    {
      "id": "find-proposals-to-review-ex2",
      "language": "bash",
      "code": "iris --json agent-playbooks proposals list --view inbox --sort-by updatedAt --sort-order desc \\\n  --page-size 50 | jq .",
      "section_id": "find-proposals-to-review"
    },
    {
      "id": "find-proposals-to-review-ex3",
      "language": "python",
      "code": "related = playbook.client.search_proposals(\n    query=\"Contact support about a missing refund\",\n    min_score=0.7,\n    action_type=\"ADD\",\n    entry_kind=\"FAQ\",\n    limit=5,\n)\nfor match in related.items:\n    print(match.proposal.proposal_id, match.score, match.proposal.action)",
      "section_id": "find-proposals-to-review"
    },
    {
      "id": "find-proposals-to-review-ex4",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  -X POST \"$PLAYBOOK_API/proposals/search\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' \\\n  --data '{\"query\":\"Contact support about a missing refund\",\"minScore\":0.7,\"actionType\":\"ADD\",\"entryKind\":\"FAQ\",\"limit\":5}' \\\n  | jq '.items[] | {proposalId: .proposal.proposalId, score, action: .proposal.action}'",
      "section_id": "find-proposals-to-review"
    },
    {
      "id": "find-proposals-to-review-ex5",
      "language": "bash",
      "code": "iris --json agent-playbooks proposals search 'Contact support about a missing refund' \\\n  --min-score 0.7 --action-type add --entry-type faq --limit 5 | jq .items",
      "section_id": "find-proposals-to-review"
    },
    {
      "id": "create-a-proposal-ex0",
      "language": "python",
      "code": "action = {\n    \"add\": {\n        \"target\": {\"new_entry\": {}},\n        \"candidate\": {\n            \"content\": {\n                \"intents\": [\"Contact support about a missing refund\"],\n                \"attributes\": {\"locale\": \"en-US\", \"topic\": \"refund\"},\n                \"payload\": {\n                    \"faq\": {\n                        \"question\": \"What should I send support about a missing refund?\",\n                        \"answer\": \"Send your order number and the date you requested the refund.\",\n                    }\n                },\n            }\n        },\n    }\n}\nproposal = playbook.client.create_proposal(\n    action=action,\n    source={\n        \"kind\": \"document\",\n        \"reference\": {\"documentId\": \"sample-refund-policy\"},\n    },\n)\nproposal_id = proposal.proposal_id\nprint(proposal_id, proposal.revision)",
      "section_id": "create-a-proposal"
    },
    {
      "id": "create-a-proposal-ex1",
      "language": "bash",
      "code": "cat > proposal.json <<'JSON'\n{\"source\":{\"kind\":\"document\",\"reference\":{\"documentId\":\"sample-refund-policy\"}},\n\"action\":{\"add\":{\n  \"target\":{\"newEntry\":{}},\n  \"candidate\":{\"content\":{\n    \"intents\":[\"Contact support about a missing refund\"],\n    \"attributes\":{\"locale\":\"en-US\",\"topic\":\"refund\"},\n    \"payload\":{\"faq\":{\n      \"question\":\"What should I send support about a missing refund?\",\n      \"answer\":\"Send your order number and the date you requested the refund.\"\n    }}\n  }}\n}}}\nJSON\ncurl --fail-with-body --silent --show-error \\\n  -X POST \"$PLAYBOOK_API/proposals\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' --data-binary @proposal.json > proposal-created.json\nPROPOSAL_ID=$(jq -er '.proposalId' proposal-created.json)\njq '{proposalId, revision}' proposal-created.json",
      "section_id": "create-a-proposal"
    },
    {
      "id": "create-a-proposal-ex2",
      "language": "bash",
      "code": "cat > proposal-faq.json <<'JSON'\n{\n  \"question\": \"What should I send support about a missing refund?\",\n  \"answer\": \"Send your order number and the date you requested the refund.\",\n  \"intents\": [\"Contact support about a missing refund\"],\n  \"attributes\": {\"locale\": \"en-US\", \"topic\": \"refund\"}\n}\nJSON\ncat > proposal-source.json <<'JSON'\n{\n  \"kind\": \"document\",\n  \"reference\": {\"documentId\": \"sample-refund-policy\"}\n}\nJSON\niris --json agent-playbooks proposals create proposal-faq.json --source proposal-source.json > proposal-created.json\nPROPOSAL_ID=$(jq -er '.proposalId' proposal-created.json)\njq '{proposalId, revision}' proposal-created.json",
      "section_id": "create-a-proposal"
    },
    {
      "id": "attach-evidence-ex0",
      "language": "python",
      "code": "from datetime import datetime, timezone\nfrom uuid import uuid4\n\nevidence_id = \"evd_\" + uuid4().hex\nobserved_at = datetime.now(timezone.utc)\nevidence = playbook.client.submit_proposal_evidence(\n    proposal_id=proposal_id,\n    evidence_id=evidence_id,\n    observed_at=observed_at,\n    producer={\"name\": \"docs-example\", \"prompt_version\": \"refund-review-v1\"},\n    source={\n        \"kind\": \"document\",\n        \"reference\": {\"documentId\": \"sample-refund-policy\"},\n        \"content\": {\n            \"excerpt\": (\n                \"For refund inquiries, collect the order number and request date. \"\n                \"Do not request full card details.\"\n            )\n        },\n    },\n)\nprint(evidence.disposition)",
      "section_id": "attach-evidence"
    },
    {
      "id": "attach-evidence-ex1",
      "language": "bash",
      "code": "EVIDENCE_ID=\"evd_$(openssl rand -hex 16)\"\njq -n --arg observedAt \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" '{\n  observedAt: $observedAt,\n  producer: {name: \"docs-example\", promptVersion: \"refund-review-v1\"},\n  source: {\n    kind: \"document\",\n    reference: {documentId: \"sample-refund-policy\"},\n    content: {excerpt: \"For refund inquiries, collect the order number and request date. Do not request full card details.\"}\n  }\n}' > evidence.json\ncurl --fail-with-body --silent --show-error \\\n  -X PUT \"$PLAYBOOK_API/proposals/$PROPOSAL_ID/evidence/$EVIDENCE_ID\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' --data-binary @evidence.json | jq .disposition",
      "section_id": "attach-evidence"
    },
    {
      "id": "attach-evidence-ex2",
      "language": "bash",
      "code": "EVIDENCE_ID=\"evd_$(openssl rand -hex 16)\"\njq -n --arg observedAt \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" '{\n  observedAt: $observedAt,\n  producer: {name: \"docs-example\", promptVersion: \"refund-review-v1\"},\n  source: {\n    kind: \"document\",\n    reference: {documentId: \"sample-refund-policy\"},\n    content: {\n      excerpt: \"For refund inquiries, collect the order number and request date. Do not request full card details.\"\n    }\n  }\n}' > evidence.json\niris --json agent-playbooks proposals evidence \"$PROPOSAL_ID\" \"$EVIDENCE_ID\" --input evidence.json | jq .disposition",
      "section_id": "attach-evidence"
    },
    {
      "id": "review-and-edit-a-candidate-ex0",
      "language": "python",
      "code": "reviewed = playbook.client.get_proposal(proposal_id=proposal_id)\nprint(reviewed.model_dump_json(by_alias=True, indent=2))",
      "section_id": "review-and-edit-a-candidate"
    },
    {
      "id": "review-and-edit-a-candidate-ex1",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  \"$PLAYBOOK_API/proposals/$PROPOSAL_ID\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" > reviewed.json\njq . reviewed.json",
      "section_id": "review-and-edit-a-candidate"
    },
    {
      "id": "review-and-edit-a-candidate-ex2",
      "language": "bash",
      "code": "iris --json agent-playbooks proposals show \"$PROPOSAL_ID\" > reviewed.json\njq . reviewed.json",
      "section_id": "review-and-edit-a-candidate"
    },
    {
      "id": "review-and-edit-a-candidate-ex3",
      "language": "python",
      "code": "revised_action = reviewed.action.model_dump(exclude_unset=True)\nrevised_action[\"add\"][\"candidate\"][\"content\"][\"payload\"][\"faq\"][\"answer\"] += (\n    \" Do not send full card details.\"\n)\nrevised = playbook.client.replace_proposal(\n    proposal_id=proposal_id,\n    action=revised_action,\n    expected_revision=reviewed.revision,\n)\nprint(revised.revision, revised.action)",
      "section_id": "review-and-edit-a-candidate"
    },
    {
      "id": "review-and-edit-a-candidate-ex4",
      "language": "bash",
      "code": "jq --arg revision \"$(jq -er '.revision' reviewed.json)\" '\n  {action} |\n  .action.add.candidate.content.payload.faq.answer += \" Do not send full card details.\" |\n  .expectedRevision = $revision\n' reviewed.json > proposal-update.json\ncurl --fail-with-body --silent --show-error \\\n  -X PATCH \"$PLAYBOOK_API/proposals/$PROPOSAL_ID\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' --data-binary @proposal-update.json > revised.json\njq '{revision, action}' revised.json",
      "section_id": "review-and-edit-a-candidate"
    },
    {
      "id": "review-and-edit-a-candidate-ex5",
      "language": "bash",
      "code": "REVISION=$(jq -er '.revision' reviewed.json)\njq '\n  .action.add.candidate.content |\n  {\n    question: .payload.faq.question,\n    answer: (.payload.faq.answer + \" Do not send full card details.\"),\n    intents: (.intents // []),\n    attributes: (.attributes // {}),\n    metadata: .metadata\n  }\n' reviewed.json > proposal-update.json\nENTRY_KEY=$(jq -r '.action.add.target.newEntry.entryKey // empty' reviewed.json)\nif [[ -n \"$ENTRY_KEY\" ]]; then\n  jq --arg key \"$ENTRY_KEY\" '.entryKey = $key' proposal-update.json > proposal-keyed.json\n  mv proposal-keyed.json proposal-update.json\nfi\niris --json agent-playbooks proposals replace \"$PROPOSAL_ID\" proposal-update.json \\\n  --expected-revision \"$REVISION\" > revised.json\njq '{revision, action}' revised.json",
      "section_id": "review-and-edit-a-candidate"
    },
    {
      "id": "dismiss-restore-or-purge-ex0",
      "language": "python",
      "code": "playbook.client.dismiss_proposal(\n    proposal_id=proposal_id,\n    expected_revision=revised.revision,\n)\nplaybook.client.restore_proposal(proposal_id=proposal_id)\n# To purge a trashed proposal instead of restoring it:\n# playbook.client.purge_proposal(proposal_id=proposal_id)",
      "section_id": "dismiss-restore-or-purge"
    },
    {
      "id": "dismiss-restore-or-purge-ex1",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  -X PUT \"$PLAYBOOK_API/proposals/$PROPOSAL_ID/dismissal\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' \\\n  --data \"$(jq '{expectedRevision: .revision}' revised.json)\" | jq .state\ncurl --fail-with-body --silent --show-error \\\n  -X DELETE \"$PLAYBOOK_API/proposals/$PROPOSAL_ID/dismissal\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" | jq .state\n# To purge instead of restoring, DELETE /proposals/$PROPOSAL_ID.",
      "section_id": "dismiss-restore-or-purge"
    },
    {
      "id": "dismiss-restore-or-purge-ex2",
      "language": "bash",
      "code": "REVISION=$(jq -er '.revision' revised.json)\niris agent-playbooks proposals dismiss \"$PROPOSAL_ID\" --expected-revision \"$REVISION\"\niris agent-playbooks proposals restore \"$PROPOSAL_ID\"\n# To purge a trashed proposal instead of restoring it:\n# iris agent-playbooks proposals purge \"$PROPOSAL_ID\" --yes",
      "section_id": "dismiss-restore-or-purge"
    },
    {
      "id": "accept-and-publish-ex0",
      "language": "python",
      "code": "final_review = playbook.client.get_proposal(proposal_id=proposal_id)\nprint(final_review.action, final_review.evidence)",
      "section_id": "accept-and-publish"
    },
    {
      "id": "accept-and-publish-ex1",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  \"$PLAYBOOK_API/proposals/$PROPOSAL_ID\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" > final-review.json\njq '{action, evidence, revision}' final-review.json",
      "section_id": "accept-and-publish"
    },
    {
      "id": "accept-and-publish-ex2",
      "language": "bash",
      "code": "iris --json agent-playbooks proposals show \"$PROPOSAL_ID\" > final-review.json\njq '{action, evidence, revision}' final-review.json",
      "section_id": "accept-and-publish"
    },
    {
      "id": "accept-and-publish-ex3",
      "language": "python",
      "code": "from redis_agent_playbook import models\n\naccepted = playbook.client.accept_proposal(\n    proposal_id=proposal_id,\n    expected_revision=final_review.revision,\n)\nif not isinstance(accepted.result, models.VersionCreated):\n    raise ValueError(\"Review the unrecognized acceptance result before publishing\")\nreceipt = accepted.result.version_created\naccepted_version = playbook.client.get_faq_version(\n    faq_id=receipt.entry_id,\n    version=receipt.version,\n)\nprint(receipt.entry_id, receipt.version, accepted_version.content)",
      "section_id": "accept-and-publish"
    },
    {
      "id": "accept-and-publish-ex4",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  -X POST \"$PLAYBOOK_API/proposals/$PROPOSAL_ID/acceptance\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' \\\n  --data \"$(jq '{expectedRevision: .revision}' final-review.json)\" > accepted.json\nACCEPTED_FAQ_ID=$(jq -er '.result.versionCreated.entryId' accepted.json)\nACCEPTED_VERSION=$(jq -er '.result.versionCreated.version' accepted.json)\ncurl --fail-with-body --silent --show-error \\\n  \"$PLAYBOOK_API/faqs/$ACCEPTED_FAQ_ID/versions/$ACCEPTED_VERSION\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  | jq '{entryId, version, content}'",
      "section_id": "accept-and-publish"
    },
    {
      "id": "accept-and-publish-ex5",
      "language": "bash",
      "code": "REVISION=$(jq -er '.revision' final-review.json)\niris --json agent-playbooks proposals accept \"$PROPOSAL_ID\" --expected-revision \"$REVISION\" > accepted.json\nACCEPTED_FAQ_ID=$(jq -er '.result.versionCreated.entryId' accepted.json)\nACCEPTED_VERSION=$(jq -er '.result.versionCreated.version' accepted.json)\niris --json agent-playbooks faqs show \"$ACCEPTED_FAQ_ID\" --version \"$ACCEPTED_VERSION\" | jq '{entryId, version, content}'",
      "section_id": "accept-and-publish"
    },
    {
      "id": "accept-and-publish-ex6",
      "language": "python",
      "code": "playbook.client.publish_faq(faq_id=receipt.entry_id, version=receipt.version)",
      "section_id": "accept-and-publish"
    },
    {
      "id": "accept-and-publish-ex7",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  -X PUT \"$PLAYBOOK_API/faqs/$ACCEPTED_FAQ_ID/publication\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' \\\n  --data \"$(jq '{version: .result.versionCreated.version}' accepted.json)\" | jq .publication",
      "section_id": "accept-and-publish"
    },
    {
      "id": "accept-and-publish-ex8",
      "language": "bash",
      "code": "iris agent-playbooks faqs publish \"$ACCEPTED_FAQ_ID\" --version \"$ACCEPTED_VERSION\"",
      "section_id": "accept-and-publish"
    },
    {
      "id": "inspect-promotion-history-ex0",
      "language": "python",
      "code": "cursor = None\nwhile True:\n    page = playbook.client.list_proposals(\n        view=\"TRASH\", sort_by=\"updatedAt\", sort_order=\"DESC\", limit=50, cursor=cursor\n    )\n    for item in page.items:\n        print(item.proposal_id, item.state, item.evidence_count, item.action)\n    cursor = page.next_cursor\n    if cursor is None:\n        break",
      "section_id": "inspect-promotion-history"
    },
    {
      "id": "inspect-promotion-history-ex1",
      "language": "bash",
      "code": "CURSOR=''\nwhile true; do\n  PAGE_ARGS=(--data-urlencode view=TRASH --data-urlencode sortBy=updatedAt --data-urlencode sortOrder=DESC --data-urlencode limit=50)\n  if [[ -n \"$CURSOR\" ]]; then PAGE_ARGS+=(--data-urlencode \"cursor=$CURSOR\"); fi\n  curl --fail-with-body --silent --show-error --get \"$PLAYBOOK_API/proposals\" \\\n    -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \"${PAGE_ARGS[@]}\" > trash.json\n  jq '.items[] | {proposalId, state, evidenceCount, action}' trash.json\n  CURSOR=$(jq -r '.nextCursor // empty' trash.json)\n  [[ -n \"$CURSOR\" ]] || break\ndone",
      "section_id": "inspect-promotion-history"
    },
    {
      "id": "inspect-promotion-history-ex2",
      "language": "bash",
      "code": "iris --json agent-playbooks proposals list --view trash --sort-by updatedAt --sort-order desc \\\n  --page-size 50 | jq .",
      "section_id": "inspect-promotion-history"
    },
    {
      "id": "review-skill-proposals-ex0",
      "language": "python",
      "code": "import base64\nfrom pathlib import Path\n\nskill_proposal = playbook.client.create_proposal(action={\n    \"add\": {\n        \"target\": {\"new_entry\": {}},\n        \"candidate\": {\"content\": {\n            \"attributes\": {\"locale\": \"en-US\", \"topic\": \"refund\"},\n            \"payload\": {\"skill_package\": {\n                \"package_base64\": base64.b64encode(Path(\"refund-support-reply.zip\").read_bytes()).decode(\"ascii\"),\n            }},\n        }},\n    },\n})\nskill_proposal_id = skill_proposal.proposal_id\nprint(skill_proposal_id, skill_proposal.revision)",
      "section_id": "review-skill-proposals"
    },
    {
      "id": "review-skill-proposals-ex1",
      "language": "bash",
      "code": "jq -n --rawfile package <(base64 < refund-support-reply.zip | tr -d '\\n') '{\n  action: {add: {\n    target: {newEntry: {}},\n    candidate: {content: {\n      attributes: {locale: \"en-US\", topic: \"refund\"},\n      payload: {skillPackage: {packageBase64: $package}}\n    }}\n  }}\n}' > skill-proposal.json\ncurl --fail-with-body --silent --show-error \\\n  -X POST \"$PLAYBOOK_API/proposals\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' --data-binary @skill-proposal.json > skill-proposal-created.json\nSKILL_PROPOSAL_ID=$(jq -er '.proposalId' skill-proposal-created.json)\njq '{proposalId, revision}' skill-proposal-created.json",
      "section_id": "review-skill-proposals"
    },
    {
      "id": "review-skill-proposals-ex2",
      "language": "python",
      "code": "import hashlib\n\nskill_review = playbook.client.get_proposal(proposal_id=skill_proposal_id)\ncandidate = skill_review.action.add.candidate.content.payload.skill\nprint(candidate.manifest, candidate.instructions)\nresource = next(r for r in candidate.resources if r.path == \"references/reply-template.md\")\nresource_response = playbook.client.get_proposal_resource(\n    proposal_id=skill_proposal_id,\n    resource_id=resource.resource_id,\n    expected_revision=skill_review.revision,\n)\ntry:\n    template_bytes = resource_response.result.read()\nfinally:\n    resource_response.result.close()\nif \"sha256:\" + hashlib.sha256(template_bytes).hexdigest() != resource.digest:\n    raise ValueError(\"Proposal resource digest does not match the reviewed descriptor\")\nprint(template_bytes.decode(\"utf-8\"))",
      "section_id": "review-skill-proposals"
    },
    {
      "id": "review-skill-proposals-ex3",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  \"$PLAYBOOK_API/proposals/$SKILL_PROPOSAL_ID\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" > skill-reviewed.json\njq '.action.add.candidate.content.payload.skill | {manifest, instructions, resources}' skill-reviewed.json\nRESOURCE_ID=$(jq -er '.action.add.candidate.content.payload.skill.resources[] | select(.path == \"references/reply-template.md\") | .resourceId' skill-reviewed.json)\nSKILL_REVISION=$(jq -er '.revision' skill-reviewed.json)\ncurl --fail-with-body --silent --show-error --get \\\n  \"$PLAYBOOK_API/proposals/$SKILL_PROPOSAL_ID/resources/$RESOURCE_ID\" \\\n  --data-urlencode \"expectedRevision=$SKILL_REVISION\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" --output reply-template.reviewed.md\nEXPECTED_DIGEST=$(jq -er --arg id \"$RESOURCE_ID\" '.action.add.candidate.content.payload.skill.resources[] | select(.resourceId == $id) | .digest' skill-reviewed.json)\nACTUAL_DIGEST=\"sha256:$(openssl dgst -sha256 reply-template.reviewed.md | awk '{print $NF}')\"\n[[ \"$ACTUAL_DIGEST\" == \"$EXPECTED_DIGEST\" ]] || { echo 'Proposal resource digest mismatch' >&2; exit 1; }\ncat reply-template.reviewed.md",
      "section_id": "review-skill-proposals"
    },
    {
      "id": "review-skill-proposals-ex4",
      "language": "python",
      "code": "accepted_skill = playbook.client.accept_proposal(\n    proposal_id=skill_proposal_id, expected_revision=skill_review.revision,\n)\nif not isinstance(accepted_skill.result, models.VersionCreated):\n    raise ValueError(\"Review the unrecognized acceptance result before publishing\")\nskill_receipt = accepted_skill.result.version_created\nif skill_receipt.entry_kind != \"SKILL\":\n    raise ValueError(\"Expected a skill receipt\")\nskill_version = playbook.client.get_skill_version(\n    skill_id=skill_receipt.entry_id, version=skill_receipt.version,\n)\nprint(skill_receipt.entry_id, skill_receipt.version, skill_version.content)",
      "section_id": "review-skill-proposals"
    },
    {
      "id": "review-skill-proposals-ex5",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  -X POST \"$PLAYBOOK_API/proposals/$SKILL_PROPOSAL_ID/acceptance\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' \\\n  --data \"$(jq '{expectedRevision: .revision}' skill-reviewed.json)\" > skill-accepted.json\njq -e '.result.versionCreated.entryKind == \"SKILL\"' skill-accepted.json > /dev/null\nACCEPTED_SKILL_ID=$(jq -er '.result.versionCreated.entryId' skill-accepted.json)\nACCEPTED_SKILL_VERSION=$(jq -er '.result.versionCreated.version' skill-accepted.json)\ncurl --fail-with-body --silent --show-error \\\n  \"$PLAYBOOK_API/skills/$ACCEPTED_SKILL_ID/versions/$ACCEPTED_SKILL_VERSION\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" | jq .content",
      "section_id": "review-skill-proposals"
    },
    {
      "id": "review-skill-proposals-ex6",
      "language": "python",
      "code": "playbook.client.publish_skill(skill_id=skill_receipt.entry_id, version=skill_receipt.version)",
      "section_id": "review-skill-proposals"
    },
    {
      "id": "review-skill-proposals-ex7",
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  -X PUT \"$PLAYBOOK_API/skills/$ACCEPTED_SKILL_ID/publication\" \\\n  -H \"Authorization: Bearer $IRIS_AGENT_PLAYBOOKS_API_KEY\" \\\n  -H 'Content-Type: application/json' \\\n  --data \"$(jq '{version: .result.versionCreated.version}' skill-accepted.json)\" | jq .publication",
      "section_id": "review-skill-proposals"
    }
  ]
}
