---
title: "Create a new intent spec (status defaults to draft) under a product in the authenticated workspace."
method: POST
path: "/api/v1/intents"
tags: ["intents"]
---

# Create a new intent spec (status defaults to draft) under a product in the authenticated workspace.

`POST /api/v1/intents`

Rate-limited by the 'intents' bucket (200/h sliding window, keyed user:<userId> or ip:<ip>). Billing-gated: when the workspace intent quota is reached, returns 403 with body { error: 'plan_limit_reached', metric: 'intents', current, limit, plan }. New intent always starts at status 'draft', version 1. Fires a Slack 'intent_status_changed' notification and an 'intent_created' analytics event (both fire-and-forget). Response is built from a re-fetch with relations (INTENT_WITH_RELATIONS_SELECT) so edgeCases are populated; evidenceIds empty on a fresh create.

## Query parameters

- `workspace_id` string

## Request body

- object
  - `id` string — Adopt this UUID instead of minting one. Exists so a spec keeps its identity when it moves from a local file into a workspace: a keyless author may already have stamped intent/<uuid> on a branch, and a changed id would silently stop the merge webhook resolving it. Rejected with 409 INTENT_ID_TAKEN if the id already exists.
  - `title` string, required — Non-empty after trim. Stored as user_goal.
  - `objective` string, required — Non-empty after trim.
  - `currentState` string — Author-written as-is behavior: how this works today, before the change (max 4000 chars). Leave out for net-new capability. Distinct from implementationContext, which is analyzer-derived.
  - `productId` string — Non-empty after trim. Must reference a product in this workspace or returns 404. Omit it and the server resolves it when the workspace has exactly one NON-example product; otherwise the request fails 400 with code PRODUCT_REQUIRED and the candidate list, so the caller can ask and retry with an explicit id. Deliberately strict: workspaces auto-load a demo product, and a per-product GitHub binding decides which repository the PR stamp and merge verification apply to, so a guessed product is a mis-wired verification loop rather than a tidy-up.
  - `outcomes` string[] — Strings or structured objects; strings must be non-empty after trim. Normalized to StructuredOutcome[] at the boundary.
  - `constraints` string[] — Coerced to trimmed non-empty strings.
  - `healthMetrics` string[] — Coerced to trimmed non-empty strings.
  - `edgeCases` object[] — Each field non-empty after trim. Synced via replaceIntentEdgeCasesRpc; on failure the just-created intent is deleted and 500 returned.
  - `verification` object — Verification plan object; defaults to {} if omitted.
  - `problemSeverity` 'low' | 'medium' | 'high' | 'critical' — Optional severity enum.
  - `scope` string — Optional scope object; may be null.

## Response `201`

Created — returns the full mapped intent

- object

## Other responses

- `400` — Invalid request body (zod) — returns { error, details } with flattened zod errors; OR cookie-session auth missing workspace_id
- `401` — Invalid/expired API key, or no Bearer key and no valid session cookie
- `403` — Lacks write scope; OR plan_limit_reached (billing intents quota exceeded); OR cookie auth not a member of the workspace
- `404` — Product not found in this workspace
- `409` — code INTENT_ID_TAKEN: the supplied id already exists. If it is yours, update it instead of creating it.
- `429` — Rate limit exceeded (intents bucket, 200/h) — { error } with Retry-After header
- `500` — Insert error, edge-case sync failure (intent rolled back/deleted), or unexpected internal error

---

[API](https://skmtc.dev/pathmode/apis/pathmode-api.md) · [All operations](https://skmtc.dev/pathmode/apis/pathmode-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/pathmode/pathmode-api/revisions/e419abb0a327/schema)
