Skip to content

UN-2190 [FEAT] Auto-capture execution ID in the API deployment Postman collection#2031

Open
athul-rs wants to merge 3 commits into
mainfrom
UN-2190-postman-collection-scripts
Open

UN-2190 [FEAT] Auto-capture execution ID in the API deployment Postman collection#2031
athul-rs wants to merge 3 commits into
mainfrom
UN-2190-postman-collection-scripts

Conversation

@athul-rs

@athul-rs athul-rs commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

What

  • The downloaded Postman collection for API deployments now wires the two requests together: the Process document request stores execution_id from its response into a collection variable via a post-response script, and the Execution status request reads {{execution_id}} from that variable. No more manual copy-pasting of execution IDs.

Why

UN-2190 — minor UX gap: after executing a document asynchronously, users had to copy the execution ID from the response and paste it into the status request's REPLACE_WITH_EXECUTION_ID placeholder. The LLMWhisperer Postman collection already does this with whisper_hash; this applies the same pattern.

How

  • postman_collection/dto.py: new ScriptItem/EventItem/VariableItem dataclasses; PostmanItem gets an optional event; the execute request carries a post-response script (pm.collectionVariables.set("execution_id", ...), guarded so non-JSON/error responses are ignored); the status URL uses {{execution_id}} (urlencode with safe="{}" so Postman's braces survive); collection-level variable block added with a REPLACE_WITH_EXECUTION_ID default so manual use still works.
  • to_dict() strips event: null from items that have no scripts.
  • Pipeline collections (single request, no status call) are unchanged.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. This only changes the generated Postman collection JSON (a download artifact), not any API behavior. The status request still works manually: if the script never ran, the variable resolves to the REPLACE_WITH_EXECUTION_ID default — identical to today's placeholder. The script is defensive (checks response.message.execution_id exists before setting).

Database Migrations

  • None

Env Config

  • None

Relevant Docs

  • N/A

Related Issues or PRs

  • Jira: UN-2190

Notes on Testing

  • ruff check + format clean.
  • Verified generated JSON structure with a stubbed-Django harness: execute item carries the event block, status item has none, status URL contains execution_id={{execution_id}} unescaped, and the collection-level variable is present.
  • Manual: download a collection from an API deployment, import into Postman, run Process document then Execution status — the second request uses the captured ID.

Screenshots

N/A

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

Add a post-response script to the 'Process document' request that
stores message.execution_id into a collection variable, and point the
'Execution status' request's execution_id query param at that variable.
Users no longer copy-paste execution IDs between requests (mirrors the
LLMWhisperer collection's whisper_hash pattern).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3a9f5dbd-f056-4814-99f0-48ad01c1215a

📥 Commits

Reviewing files that changed from the base of the PR and between fe248a4 and 4bafbf7.

📒 Files selected for processing (1)
  • backend/api_v2/serializers.py

Summary by CodeRabbit

  • New Features
    • Postman collections now include collection-level variables and test-event scripting to capture and propagate an execution identifier automatically.
    • Status requests are wired to use the captured execution identifier via an encoded variable placeholder for end-to-end execution tracking.
    • API responses for execution now include an execution identifier field so callers can correlate execute and status flows.

Walkthrough

Adds constants and DTOs for Postman script events and collection variables, extends collection/item models, seeds collection variables via APIBase, and wires a test event on the execute request to capture execution_id for use in status requests.

Changes

Postman Execution ID Variable Flow

Layer / File(s) Summary
DTOs and constants (contracts)
backend/api_v2/postman_collection/dto.py, backend/api_v2/postman_collection/constants.py
Adds ScriptItem, EventItem, VariableItem dataclasses; adds CollectionKey.EXEC_ID_VARIABLE_NAME and CollectionKey.STATUS_EXEC_ID_VARIABLE; adds variable field to PostmanCollection.
Collection & item model updates
backend/api_v2/postman_collection/dto.py
PostmanItem adds optional event; PostmanCollection.create() populates variable from get_collection_variables(); to_dict() omits event when None.
APIBase hook and collection initialization
backend/api_v2/postman_collection/dto.py
Adds APIBase.get_collection_variables() hook returning list of VariableItem (default empty).
Execute -> status wiring and URL encoding
backend/api_v2/postman_collection/dto.py
Status URL encoding preserves {{execution_id}}; execute request item is augmented with a Postman test event that parses response.message.execution_id and stores it into the collection variable referenced by the status request.
Serializer: expose execution_id
backend/api_v2/serializers.py
APIExecutionResponseSerializer adds execution_id = CharField() to serialized execute responses.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: auto-capturing execution ID in the API deployment Postman collection by wiring together two requests via a post-response script.
Description check ✅ Passed The PR description is comprehensive and complete, covering all template sections: What, Why, How, breakage analysis, migrations, env config, docs, related issues, testing notes, and checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch UN-2190-postman-collection-scripts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires together the two API-deployment requests in the downloaded Postman collection so that the execution_id returned by the execute request is automatically captured and fed into the status-polling request — eliminating the manual copy-paste step. All three previous review threads (unguarded pm.response.json(), spurious variable on Pipeline collections, missing execution_id field in the serializer) have been addressed in the current diff.

  • dto.py: Adds ScriptItem/EventItem/VariableItem dataclasses; PostmanItem gains an optional event list; APIDeploymentDto attaches a defensive post-response test script and overrides get_collection_variables() to seed the fallback default; PostmanCollection.to_dict() strips event: null entries; PipelineDto inherits the no-op base implementation and serialises with "variable": [].
  • serializers.py: Adds execution_id = CharField() to APIExecutionResponseSerializer so the field is present in the response body that the Postman script reads from response.message.execution_id.

Confidence Score: 5/5

Safe to merge — changes only affect the generated Postman collection JSON, not any live API behaviour; the serializer addition is purely additive.

The three substantive issues identified in the previous review round (unguarded JSON parse, spurious Pipeline variable, missing execution_id field) have all been resolved cleanly. The execute response structure matches what the Postman script reads. The Pipeline code path inherits a no-op get_collection_variables() and emits an empty "variable": [] which Postman accepts without issue. The serializer change exposes a field that was already present on ExecutionResponse but previously omitted from the output — additive and non-breaking.

No files require special attention.

Important Files Changed

Filename Overview
backend/api_v2/postman_collection/dto.py Core change: adds new dataclasses, attaches a try/catch-guarded Postman test script to the execute item, seeds a collection variable with a fallback default, and strips null event blocks in to_dict(). Logic is sound after previous-round fixes.
backend/api_v2/serializers.py Adds execution_id = CharField() to APIExecutionResponseSerializer; additive and non-breaking since ExecutionResponse.execution_id is always a non-null UUID string.
backend/api_v2/postman_collection/constants.py Adds two constants (EXEC_ID_VARIABLE_NAME and STATUS_EXEC_ID_VARIABLE) used to avoid magic strings; straightforward and correct.

Sequence Diagram

sequenceDiagram
    participant User as Postman User
    participant Execute as Process Document Request
    participant Script as Post-Response Script
    participant CollVar as Collection Variable (execution_id)
    participant Status as Execution Status Request
    participant API as Unstract API

    User->>Execute: "POST /api/{name} (multipart file)"
    Execute->>API: HTTP POST
    API-->>Execute: "{"message": {"execution_id": "uuid", "execution_status": "..."}}"
    Execute->>Script: Run test script
    Script->>Script: "try { pm.response.json() }"
    Script->>CollVar: pm.collectionVariables.set("execution_id", uuid)
    User->>Status: "GET /api/{name}?execution_id={{execution_id}}"
    Status->>CollVar: "Resolve {{execution_id}} to uuid"
    Status->>API: HTTP GET with resolved UUID
    API-->>User: Execution status response
Loading

Reviews (3): Last reviewed commit: "UN-2190 Expose execution_id in the API e..." | Re-trigger Greptile

Comment thread backend/api_v2/postman_collection/dto.py
Comment thread backend/api_v2/postman_collection/dto.py Outdated
…PI deployments

- Wrap pm.response.json() in try/catch so error pages (non-JSON) don't
  surface a Postman test error
- Move collection variables behind APIBase.get_collection_variables()
  so Pipeline collections (no status request) stay variable-free

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread backend/api_v2/postman_collection/dto.py
The ExecutionResponse DTO carries execution_id but
APIExecutionResponseSerializer dropped it, so the Postman capture
script (and any API consumer) had to parse it out of status_api.
Add it as a first-class response field; the collection script's
message.execution_id lookup now matches the real payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
unit-connectors unit 64 12 0 3 16.7
unit-core unit 0 0 2 0 1.2
unit-platform-service unit 9 0 1 0 1.3
unit-prompt-service unit 15 0 0 0 19.5
unit-rig unit 53 0 0 0 3.3
unit-runner unit 11 0 0 0 3.1
unit-sdk1 unit 381 0 0 0 23.6
unit-tool-registry unit 0 0 1 0 1.3
unit-workers unit 0 0 0 0 17.5
TOTAL 533 12 4 3 87.5

Critical paths

⚠️ Critical paths not yet covered

  • auth-login — User can log in and obtain a session cookie. (entry: POST /api/v1/auth/login; declared coverage: no groups declared)
  • adapter-register-llm — Register and validate an LLM adapter. (entry: POST /api/v1/adapter/; declared coverage: no groups declared)
  • workflow-create-execute — Create a workflow, configure source+destination, execute, poll, fetch result. (entry: POST /api/v1/workflow/{id}/execute/; declared coverage: e2e-workflow)
  • api-deployment-run — Deploy a workflow as an API, POST a document, receive structured JSON. (entry: POST /deployment/api/{org}/{name}/; declared coverage: e2e-api-deployment)
  • prompt-studio-fetch-response — Prompt Studio: create project, add prompt, run single-pass, get response. (entry: POST /api/v1/prompt-studio/prompt-studio-tool/{id}/fetch_response/; declared coverage: e2e-prompt-studio)
  • pipeline-etl-execute — Run an ETL pipeline from source connector to destination. (entry: POST /api/v1/pipeline/{id}/execute/; declared coverage: no groups declared)
  • usage-token-tracking — Per-execution token usage is recorded and retrievable. (entry: GET /api/v1/usage/get_token_usage/; declared coverage: no groups declared)
  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (entry: internal: backend → rabbitmq → workers/file_processing; declared coverage: no groups declared)
  • callback-result-delivery — Async results are posted back via the callback worker. (entry: internal: workers/callback → backend /internal endpoints; declared coverage: no groups declared)
✅ Covered critical paths
  • tool-sandbox-exec — covered by unit-runner

@jaseemjaskp jaseemjaskp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review (PR Review Toolkit: code-reviewer, silent-failure-hunter, type-design-analyzer, pr-test-analyzer, comment-analyzer, code-simplifier).

The change is correct and well-scoped — the capture script's response.message.execution_id path is validated by the new serializer field and the {"message": ...} response envelope, and urlencode(safe="{}") correctly preserves {{execution_id}}. The already-fixed items (serializer dropping execution_id, pipeline collection variables, non-JSON try/catch) are not re-raised.

The findings below are all new. The only substantive one is the stale-variable risk (P1); the rest are P2/P3 hardening, type-design, comment, and test-coverage suggestions.

"} catch (error) {",
" // Non-JSON response (e.g. gateway error); nothing to capture",
"}",
"if (response && response.message && response.message.execution_id) {", # noqa: E501

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — silent reuse of a stale execution_id. This guard has no else branch, and it's distinct from the non-JSON try/catch already fixed below: here the response parses fine but carries no message.execution_id — e.g. a 422 validation error or 500 ({"message": {"error": ...}}), auth/rate-limit rejection, or any 2xx with an unexpected shape. In that case the script does nothing and {{execution_id}} keeps whatever it held — most dangerously the id captured from a previous successful run. The user re-runs Execute, it fails, hits Execution status, and Postman silently polls the old execution and shows its COMPLETED result as if it belonged to the failed run. No signal that capture was skipped.

Suggest making the miss explicit and preventing cross-run staleness:

if (response && response.message && response.message.execution_id) {
    pm.collectionVariables.set("execution_id", response.message.execution_id);
} else {
    pm.collectionVariables.set("execution_id", "REPLACE_WITH_EXECUTION_ID");
    console.warn("No execution_id in execute response; not reusing a stale value. Status:", pm.response.code);
}

Optionally gate capture on pm.response.code 2xx so a 200-with-unexpected-shape is surfaced too.

class PostmanItem:
name: str
request: RequestItem
event: list[EventItem] | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — event: list[EventItem] | None = None creates a None/[] tri-state where only "some events" / "no events" are meaningful, and it's the sole reason to_dict() (lines 301-306) has to post-process and pop null event keys. Other "maybe-empty" fields on these DTOs already use field(default_factory=list) (PostmanCollection.item/variable, lines 251-252). Aligning here:

event: list[EventItem] = field(default_factory=list)

lets you drop the strip loop and reduce to_dict to return asdict(self). Trade-off: items then serialize "event": [] instead of omitting the key — both are valid Postman v2.1 (empty array is the schema-canonical form). If preserving the exact "key absent" output is a hard requirement, keep the loop but make it robust to the new default by testing if not item.get("event") rather than is None.


@dataclass
class EventItem:
listen: str

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 (type safety) — constrain the closed enums. EventItem.listen accepts any str, but Postman v2.1 only recognizes two hooks; a typo like "tests" produces a silently-ignored event with no error anywhere in this pipeline. Narrow it to Literal["prerequest", "test"]. Same idea for ScriptItem.type (line 24): only "text/javascript" is meaningful, so Literal["text/javascript"] (or keep the default and type it as such). Zero runtime cost, caught by mypy, self-documenting.

]

def _get_execute_capture_event(self) -> EventItem:
"""Post-response script that stores the execution_id from the execute

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 (comment) — docstring omits the load-bearing response-shape coupling. The script silently no-ops unless the response is {message: {execution_id}}; if that envelope ever changes (e.g. message renamed or execution_id relocated) capture breaks quietly. Add a line so a future API change flags this script, e.g. "Assumes the execute response has shape {message: {execution_id}}; if the body is non-JSON or lacks that field, nothing is captured." Minor: "Post-response script" matches Postman's current UI label but the code emits listen="test" — a half-sentence tying the two ("post-response, i.e. Postman's test hook") avoids a maintainer double-take.

def get_form_data_items(self) -> list[FormDataItem]:
pass

def get_collection_variables(self) -> list["VariableItem"]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 (nit) — unnecessary string forward-reference. VariableItem is already defined above (line 33), so the quotes in -> list["VariableItem"] aren't needed; the override at line 175 already uses the bare list[VariableItem]. Use the bare type for consistency.

EXECUTE_PIPELINE_API_KEY = "Process pipeline"
STATUS_API_KEY = "Execution status"
STATUS_EXEC_ID_DEFAULT = "REPLACE_WITH_EXECUTION_ID"
EXEC_ID_VARIABLE_NAME = "execution_id"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — unenforced coupling between the variable name and its {{...}} reference. EXEC_ID_VARIABLE_NAME = "execution_id" and STATUS_EXEC_ID_VARIABLE = "{{execution_id}}" must agree (the status URL references {{execution_id}}, the capture JS sets "execution_id", and the collection variable keys on it). If one is renamed and the other isn't, the collection breaks silently — the status request would query a never-set variable. Derive one from the other so they can't drift:

STATUS_EXEC_ID_VARIABLE = f"{{{{{EXEC_ID_VARIABLE_NAME}}}}}"  # -> "{{execution_id}}"

dict[str, Any]: PostmanCollection as a dict
"""
collection_dict = asdict(self)
# Drop null event blocks; Postman expects "event" to be a list

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 (test coverage) — backend/api_v2/postman_collection/ has no tests at all, and this PR's trickiest logic is exactly the kind that fails only at Postman-import time, not in CI. to_dict() (and create()) are pure given settings.WEB_APP_ORIGIN_URL + an api_key, so this is cheap to lock down. Suggest adding backend/api_v2/postman_collection/tests/test_dto.py covering: (a) to_dict strips null event yet keeps the populated execute event and emits the variable array; (b) regression guard that PipelineDto collections carry no event key and variable == [] while APIDeploymentDto carries both; (c) the variable key, the {{execution_id}} in the status URL, and the pm.collectionVariables.set(...) in the JS all reference the same CollectionKey.EXEC_ID_VARIABLE_NAME (assert via the constant, not a hardcoded literal, so a partial rename fails); (d) the status URL keeps execution_id={{execution_id}} literal (no %7B). Assert load-bearing substrings, not the whole exec array, to stay robust to comment/wording edits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants