mcpField
C
create-disposition

create-disposition

github.com/bolna-aiData & databases365 installsUpdated 3d ago
Repository

About create-disposition

create-disposition is a Bolna skill for defining structured post-call extractions from transcripts. It helps teams classify outcomes and capture validated data, with options to test and reuse dispositions across agents.

Key features

  1. 01Create, update, and delete dispositions
  2. 02Define objective and free-text answers
  3. 03Validate typed extraction results
  4. 04Bulk-create and test dispositions
  5. 05Link dispositions to agents

Use cases

  1. 01Classify call outcomes and sentiment
  2. 02Extract contact details with validation
  3. 03Test extraction rules on transcripts

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/bolna-ai/skills --skill create-disposition

After installing, type/create-dispositionin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

assets/
sample_bulk_dispositions.json
sample_transcripts.json
references/
answer-types.md
copy-on-write.md
scripts/
bulk_create_dispositions.py
create_disposition.py
test_dispositions.py

Details

From the project README

Bolna Dispositions (Post-Call Extractions)

Dispositions turn raw transcripts into structured data after every call. Each disposition asks one question; results land in extracted_data on the execution payload and in the webhook delivery.

Hierarchy

Agent
└── Extractions (feature)
    └── Category  (e.g. "Lead Quality")
        └── Disposition  (e.g. "Call Outcome")

Category groups results visually. Disposition is the unit — one question, one answer set.

When to add a disposition

Use caseField nameAnswer type
Call outcomeCall OutcomePre-defined: interested / not_interested / follow_up / wrong_number
Lead qualification (BANT)Budget, Authority, Need, TimelineMixed — free text + Yes/No
Appointment bookedAppointment Date, Appointment TimeFree text with timestamp / text validation
SentimentCustomer SentimentPre-defined: positive / neutral / negative
Agent handover requiredHandover NeededPre-defined: Yes / No
ComplianceDisclosure Read, Consent CapturedPre-defined: Yes / No
Contact info capturedEmail, Alt PhoneFree text with email / regex validation
Open-ended summaryMain Concern, Reason for CancellationFree text

Endpoints

EndpointPurpose
GET /dispositions/List all dispositions in the account (optionally scoped to an agent)
GET /dispositions/{id}Get a single disposition
POST /dispositions/Create one disposition, optionally linking to agents
POST /dispositions/bulkCreate N dispositions for an agent atomically
PUT /dispositions/{id}Update a disposition (copy-on-write aware)
DELETE /dispositions/{id}Delete a disposition
POST /v2/agent/{agent_id}/dispositions/testTest all of an agent's dispositions against a transcript

Auth: Authorization: Bearer $BOLNA_API_KEY. JSON body for all POST/PUT.

Disposition object

{
  "name": "Call Outcome",
  "question": "What was the outcome of the call?",
  "system_prompt": "You are analyzing a sales call transcript.",
  "category": "Lead Quality",
  "model": "gpt-4.1-mini",

  "is_subjective": true,
  "is_objective": true,

  "subjective_type": "text",
  "subjective_type_config": null,

  "objective_options": [
    { "value": "interested",     "condition": "Customer expressed genuine interest and agreed to a next step" },
    { "value": "not_interested", "condition": "Customer declined all proposals" },
    { "value": "follow_up",      "condition": "Customer asked to be contacted later" }
  ],

  "agent_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"]
}
FieldTypeNotes
namestringDisplay name in extracted_data.
questionstringThe prompt the LLM evaluates against the transcript. Be specific.
system_promptstring | nullOptional system context for the evaluation LLM.
categorystringGroups outputs. Default "General".
modelstringDefault gpt-4.1-mini. Use a stronger model for high-stakes extractions.
is_subjectiveboolEnables subjective free-text response.
is_objectiveboolEnables objective pre-defined selection.
subjective_typestringFree-text constraint. See below.
subjective_type_configobjectRequired when subjective_type == "regex".
objective_optionsarrayRequired when is_objective is true.
agent_idsarrayAgents this disposition is linked to.

At least one of is_subjective or is_objective must be true.

Free-text validation types

subjective_typeWhat it validatesExample valueNotes
textAnything"Customer was happy with the demo"Default. No validation.
timestampISO 8601"2026-04-08T14:30:00"Parsed and validated.
numericInt or decimal"42", "3.14"
booleanExactly true or false"true"
emailValid RFC email"[email protected]"
regexMatches subjective_type_config.pattern"1234567890"Add pattern + optional description in subjective_type_config.

When validation fails, the original LLM response is preserved in subjective and validation.is_valid is false. No data is lost — the consumer decides whether to use it.

Example with regex (10-digit phone):

{
  "subjective_type": "regex",
  "subjective_type_config": {
    "pattern": "^\\d{10}$",
    "description": "10-digit phone number"
  }
}

Objective options

{
  "value": "interested",
  "condition": "Customer expressed genuine interest and agreed to a next step",
  "sub_options": []
}

sub_options is recursive and supports hierarchical classifications (e.g. interested → enterprise / pro / starter).

Output format on executions

{
  "execution_id": "abc123",
  "status": "completed",
  "transcript": "...",
  "extracted_data": {
    "Lead Quality": {
      "Call Outcome": {
        "subjective": "Customer expressed strong interest and asked about enterprise pricing.",
        "objective": "interested",
        "confidence": 0.92,
        "confidence_label": "High",
        "reasoning_subjective": "Customer asked about enterprise pricing and requested a demo.",
        "reasoning_objective": "Customer explicitly expressed interest and agreed to a next step.",
        "validation": null
      }
    },
    "Contact Info": {
      "Customer Email": {
        "subjective": "[email protected]",
        "objective": null,
        "confidence": 0.95,
        "confidence_label": "High",
        "reasoning_subjective": "Customer clearly provided their email address during the call.",
        "reasoning_objective": null,
        "validation": { "is_valid": true, "expected_type": "email" }
      }
    }
  }
}
FieldNotes
subjectiveFree-text response. "" when nothing found, null when is_subjective is false.
objectiveSelected pre-defined value, or null.
confidence0.0 – 1.0.
confidence_label"High" (≥0.8), "Medium" (≥0.5), "Low" (<0.5).
reasoning_subjective / reasoning_objectiveLLM's explanation per answer type. Useful for audit + debugging.
validationPresent only when subjective_type is typed (not plain text).

Same payload arrives via webhook. See https://raw.githubusercontent.com/bolna-ai/skills/main/references/execution-payload.md.

Create one disposition

curl --request POST https://api.bolna.ai/dispositions/ \
  --header "Authorization: Bearer $BOLNA_API_KEY" \
  --header "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "name": "Call Outcome",
  "question": "What was the outcome of the call?",
  "category": "Lead Quality",
  "is_subjective": true,
  "is_objective": true,
  "subjective_type": "text",
  "objective_options": [
    { "value": "interested",     "condition": "Customer expressed genuine interest and agreed to a next step" },
    { "value": "not_interested", "condition": "Customer declined all proposals" },
    { "value": "follow_up",      "condition": "Customer asked to be contacted later" }
  ],
  "agent_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"]
}
JSON

Returns the full disposition object with id. Save it if you intend to update later.

Bulk create (recommended for new agents)

Add a full analytics suite atomically — either all succeed or none do:

curl --request POST https://api.bolna.ai/dispositions/bulk \
  --header "Authorization: Bearer $BOLNA_API_KEY" \
  --header "Content-Type: application/json" \
  --data @assets/sample_bulk_dispositions.json

The asset file is a complete sales-call analytics pack: Call Outcome, Customer Sentiment, Budget mentioned, Decision Maker, Handover Needed, Customer Email.

Test before going live

Run all of an agent's dispositions against a transcript and inspect what comes out:

curl --request POST "https://api.bolna.ai/v2/agent/$AGENT_ID/dispositions/test" \
  --header "Authorization: Bearer $BOLNA_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "transcript": "Agent: Hi, how can I help? Customer: I am interested in your enterprise plan. Agent: Great — what email should I send the proposal to? Customer: priya at example dot com.",
    "call_date": "2026-05-19T10:00:00Z"
  }'

Response is the same shape as a real execution's extracted_data. Iterate on dispositions until results match what you'd want from production calls.

Updates and the copy-on-write rule

Scoped update (pass agent_id in the body or URL):

Disposition stateBehaviourStatus
Exclusive to this agentEdited in place200 OK (id unchanged)
Shared with other agentsNew private copy created, agent re-linked to copy201 Created (new id)

If you store disposition IDs in your own database, update the stored ID when you see 201. The old ID still exists — it just no longer belongs to your scoped agent.

Unscoped update (no agent_id):

  • Admins: edits any disposition in place.
  • Non-admins: only edits dispositions they own.

Authorization quirks

  • Regular users can DELETE only dispositions they created (created_by matches their user ID).
  • Admins can delete any disposition.
  • Historical executions that already contain a disposition's output are unaffected by edits or deletion — only future calls change.

Going deeper

FileContents
references/answer-types.mdFree Text vs Pre-defined, validation patterns, when to use each.
references/copy-on-write.mdWorked walkthrough of the 200 vs 201 update flow, with code.
assets/sample_bulk_dispositions.jsonDrop-in sales-call analytics pack — 6 dispositions, two categories.
assets/sample_transcripts.jsonThree transcripts (sales, support, appointment) for testing.
scripts/create_disposition.pyWraps POST /dispositions/ with flags.
scripts/bulk_create_dispositions.pyWraps POST /dispositions/bulk from a JSON file.
scripts/test_dispositions.pyWraps POST /v2/agent/{id}/dispositions/test for sample transcripts.

Legacy: gpt_assistants.custom_questions

Older agents (and some agents migrated from the v1 dashboard) carry their post-call extraction logic inline on the agent itself, not as separate disposition objects. You'll see this when GET /v2/agent/{id} returns:

{
  "dispositions": null,
  "gpt_assistants": [
    {
      "assistant_id": "asst_f4sCpG4zASXIls0FMl7PkMMt",
      "custom_questions": "1. interested:\n[- What to Extract: User interest in becoming an expert\n- Expected Response: Yes or No]\n\n2. reason_not_interested:\n[- What to Extract: Reason stated by the user...\n- Expected Response: Short reason]\n\n3. full_name:\n..."
    }
  ]
}
AspectLegacy gpt_assistantsModern dispositions
Where it livesInline on the agent, single free-text stringSeparate disposition objects linked via agent_ids
FormatNumbered list, free-text "what to extract" + "expected response" hintStructured: name, question, objective_options[], subjective_type
ValidationNone — the LLM returns whatever it interpretsTyped (timestamp, email, regex, etc.) with validation.is_valid flag
ConfidenceNoYes — confidence + confidence_label
Reusable across agentsNo — copied per agentYes — one disposition can link to N agents
Where the result landsSame extracted_data field on the executionSame extracted_data field on the execution

Both systems coexist. If dispositions is set, it runs. If dispositions is null but gpt_assistants[0].custom_questions has content, that runs instead. The output structure on the execution is the same shape either way.

Migration: when you ask the user to "add dispositions" to a legacy agent, you typically:

  1. Parse the existing custom_questions string — one numbered item per intended disposition.
  2. Convert each item into a structured disposition (POST /dispositions/).
  3. Optionally clear gpt_assistants once the new dispositions are linked, so it's unambiguous which system is producing the output.

You don't need to migrate to use modern features — but you can't get confidence scores, typed validation, or shared dispositions without it.

Common gotchas

  • Vague questions yield low confidence. "How was the call?" → noisy. "Did the customer agree to a follow-up meeting? If yes, on what date?" → clear.
  • objective_options.condition is the LLM's selection rubric. Write it as if instructing a human reviewer.
  • Don't pile many concepts into one disposition. Split "lead quality and next step" into two dispositions — outcome + action.
  • Pick the right answer type. Categorical = is_objective. Open-ended = is_subjective. Both = both flags true (lots of dispositions benefit from both).
  • Confidence ≥ 0.8 is generally trustworthy. Below 0.5 → review manually. Build CRM routing around confidence_label, not raw transcript heuristics.
  • agent_ids is the linkage. A disposition without agent_ids exists but doesn't run on any agent. Bulk-create is the easiest way to ensure every new disposition is linked.

See also

  • get-executions — fetching extracted_data after calls.
  • setup-webhook — receiving extractions in real time.
  • create-agent — top-level agent config (Extractions is a feature of the Analytics tab).
  • https://raw.githubusercontent.com/bolna-ai/skills/main/references/execution-payload.md — shape that contains extracted_data.

Comments

Markdown supported