Skip to content

OQL · Entities

Deals

OQL field reference for deals. Sales opportunities linked to contacts, tracked through pipeline stages.

Maintained by the OnePageCRM engineering team · Last updated Aug 13, 2026

Deals are sales opportunities linked to contacts and tracked through pipeline stages. The financial fields are aggregatable, which makes deals the natural entity for revenue reporting.

Fields

Legend: F filterable, S sortable, A aggregatable, G groupable.

FieldTypeFSAGDescription
idIDYDeal ID
namestringYYDeal name
textstringDeal description / notes (free text; writable via create/update). Plain text — Markdown and HTML are not rendered.
contact_idIDYYAssociated contact ID
owner_idIDYYDeal owner user ID. Defaults to the calling user on create.
statusstringYYYOne of pending, won, lost. Defaults to pending on create.
amountnumberYYYDeal value per recurring period. Equals total_amount when months is 1.
total_amountnumberYYYamount × months. Equals amount when months is 1. Derived — not writable.
costnumberYYYDeal cost per recurring period, if cost tracking is enabled. Equals total_cost when months is 1.
total_costnumberYYYcost × months. Equals cost when months is 1. Derived — not writable.
marginnumberYYYProfit margin over all recurring months (total_amount - total_cost). Derived — not writable.
commissionnumberYYYCommission as a money value, totalled over all recurring months. Pair with commission_type: "absolute"; read-only when the type is percentage.
commission_percentagenumberYYCommission percentage. Pair with commission_type: "percentage".
commission_typestringYYHow commission is expressed: none, percentage, or absolute. Inferred from whichever value field you send when omitted.
commission_basestringYYWhat percentage commission is taken from: amount (meaning total_amount) or margin. Defaults to amount.
pipeline_idIDYYPipeline ID. Defaults to the account’s default pipeline on create.
stagenumberYYYPipeline stage number. Live for pending deals only. Defaults to the resolved pipeline’s first stage when creating a pending deal.
last_stagenumber (virtual)YYFinal stage for closed deals (won, lost, and every delivery-pipeline deal)
expected_close_datedateYYYExpected close date (when status is pending). Defaults to today on create.
close_datedateYYYActual close date (when status is won or lost). Defaults to today when a deal is closed without one.
monthsnumberYNumber of recurring months, 1–100. Defaults to 1.
reason_loststring (output only)Reason lost display name
reason_lost_idIDYYReason lost ID
has_deal_itemsbooleanYWhether the deal has line items
archivedbooleanYYWhether the deal is archived
created_attimeYYYRecord creation timestamp
modified_attimeYYYLast modification timestamp

Notes

  • stage vs last_stage map to the same underlying value but are scoped by deal status. Pending deals expose stage; closed deals (won, lost, and every deal in a delivery pipeline) expose last_stage. The non-applicable one is null in projections, and filtering by stage implicitly restricts the result set to pending deals.
  • Stage values are integers but not sequential. A typical pipeline uses values like 10, 20, 40. Stage numbers and their labels are account-configurable.
  • Sales vs delivery pipelines. A pipeline is either a sales pipeline (deals run pending → won/lost) or a delivery pipeline (post-sales project tracking, where every deal is won and the stages track delivery progress). Deals in a delivery pipeline are always won, so their position lives in last_stage, not stage. Use context() to see each pipeline’s type.
  • Creating into a delivery pipeline forces status: "won". Passing "pending" alongside a delivery pipeline_id is coerced rather than rejected, so check the pipeline’s type in context() before you choose a status. Pass the position as stage on create either way — last_stage is the field you read it back from, not one you write.
  • Financial fields (amount, total_amount, cost, total_cost, margin, commission, commission_percentage) are all aggregatable and valid as the argument to sum, avg, min, max, median, and percentile.
  • Recurring deals: per-period vs total. amount and cost are per-period figures; total_amount and total_cost multiply them by months. margin is already a total (total_amount - total_cost), not amount - cost, so on a 12-month deal it is 12 times the per-period profit. Non-recurring deals have months of 1, where the per-period and total figures are equal. months accepts 1–100; out-of-range values are clamped, not rejected. The three totals are computed server-side and cannot be written — set amount (and months if it recurs) and they follow. total_amount is the figure the app shows on the deal card, so it is the one to read when the question is “what is this deal worth”.
  • Commission. commission_type decides which field carries the value: percentage uses commission_percentage, absolute uses commission as a money value, none means no commission. Omit the type and it is inferred from whichever of the two you send. Under percentage the server computes commission and it becomes read-only. commission_base picks what the percentage is taken from — amount (which means total_amount, across all recurring months, not the per-period figure) or margin — and defaults to amount. It is ignored when the type is none.
  • No currency is attached. Every money field is a bare number. The account’s currency and its symbol are in context() as account.currency and account.currency_symbol. Read them before rendering a money value — the number alone gives no indication of the unit.
  • reason_lost is a display alias, resolved from reason_lost_id. Selecting it is rejected outright — select reason_lost_id and map it through context()’s reasons_lost list for the label.
  • close_date is writable via the MCP create/update tools, but only on won or lost deals (or together with status: "won"/"lost" in the same call). Pending deals have no close date; setting it on one is rejected with a hint to use expected_close_date instead.
  • Lookups: contact (via contact_id). Pull the contact’s fields inline as contact.<field> — for example contact.last_name, contact.company_name, or contact.country_code — in select, where, and order_by. See Concepts › Lookups.
  • Custom fields are queryable by name as custom_fields.<name> in select, where, and order_by; select custom_fields.* (or *) to return all of them. Filterability and sortability depend on the field’s type. Numeric custom fields can also be aggregated ({ "sum": ["custom_fields.<name>"] }), and min/max work on date custom fields. group_by and distinct on custom fields are not supported: group by a top-level field and filter on the custom field in where instead. Use describe to discover an account’s custom-field names and types.

Example queries

Top 25 open deals by amount, each with its contact’s company (a lookup):

{
  "from": "deals",
  "select": ["name", "amount", "contact.company_name"],
  "where": { "status": "pending" },
  "order_by": [{ "amount": "desc" }],
  "limit": 25
}

Deals I own that closed last quarter:

{
  "from": "deals",
  "where": {
    "owner_id": "ME()",
    "status": "won",
    "close_date": "LAST_QUARTER()"
  }
}

Won revenue this quarter, by owner:

{
  "from": "deals",
  "select": ["owner_id", "count()", { "sum": ["amount"] }],
  "where": { "status": "won", "close_date": "THIS_QUARTER()" },
  "group_by": ["owner_id"]
}

Monthly closed-won trend over the last year:

{
  "from": "deals",
  "select": ["close_date", "count()", { "sum": ["amount"] }],
  "where": {
    "status": "won",
    "close_date": { ">=": { "DAYS_AGO": [365] } }
  },
  "group_by": [{ "MONTH": ["close_date"] }]
}

Stuck deals (in pipeline more than 90 days):

{
  "from": "deals",
  "where": {
    "status": "pending",
    "created_at": { "<": { "DAYS_AGO": [90] } }
  },
  "order_by": [{ "amount": "desc" }]
}