Skip to content

Opportunity Approvals

Opportunity Approvals gives an enabled Bombyx organization a shared review inbox. A connector can submit discoveries and suggested drafts. An authenticated organization member can edit a draft and authorize one action. A connector with execution access can then claim, revalidate, perform, and record that action.

Agents cannot approve their own work. Approval exists only in the authenticated Bombyx app.

The app opens on Review, which contains only complete, currently approvable targets. Agent-owned research, claimed work, scheduled retries, and recovery that needs attention appear under In progress. Final publication outcomes and closed items appear under History, so ordinary agent work does not look blocked. Connectors should use draft_ready only after the source, supported action, and suggested draft are ready for a member decision.

Use separate connector capabilities:

  • operator_opportunities_write submits discoveries and draft revisions.
  • operator_authorizations_execute reads and executes member-approved actions.
  • operator_learning_review reads approved revision evidence and applies a reviewed learning batch.
  • operator_draft_retries_process claims and completes member-requested draft retries. It cannot approve or post an action.
  • memory_propose is additionally required when applying a learning batch, because factual corrections become reviewable Brain proposals.

The feature must also be enabled for the organization. A valid capability does not bypass a disabled organization feature.

Send POST /app/api/tool-connectors/v1/operator/opportunities with a stable source_system and external_key. Reusing both values updates the existing organization record instead of creating a duplicate.

Use record_type: target only for a concrete public target. Use scan_outcome for useful channel-level results such as no qualified finding or an identity problem. Scan outcomes cannot be authorized directly.

{
"source_system": "scheduled_operator",
"external_key": "facebook-post-123",
"run_key": "round-2026-07-12-am",
"record_type": "target",
"platform": "facebook",
"community": "Example page",
"canonical_url": "https://www.facebook.com/example/posts/123/",
"target_key": "target:facebook:bf8f620453a8e6b8b1cc377f8e0b3dc0",
"native_target_ids": {"post_id": "123"},
"score": 0.82,
"recommendation": "act",
"action_type": "comment",
"status": "draft_ready",
"title": "Owner asks about a paint chip",
"summary": "A concise description of the opportunity.",
"rationale": "Why participation would be useful and appropriate.",
"blockers": [],
"next_action": null,
"next_action_due_at": null,
"metadata": {
"sources_required": true,
"review_context": {
"version": 1,
"visibility": "public",
"kind": "reply_target",
"author": "Original poster",
"handle": "u/example",
"text": "The public comment or post text the draft will answer.",
"parent_title": "Source thread, video, question, or post title",
"captured_at": "2026-07-12T18:00:00Z",
"content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
},
"draft": {
"content": "Suggested response text.",
"disclosure": "Organization representative.",
"metadata": {
"sources": [
{
"title": "Manufacturer repair instructions",
"organization": "Example Manufacturer",
"url": "https://example.com/repair-instructions",
"support_note": "Supports the preparation step in the draft."
}
]
}
}
}

When metadata.sources_required is true, the draft must include at least one draft-bound source. Each source has a specific title, organization, direct HTTPS URL, and an optional short support note. Bombyx validates the URL as a public HTTPS address, derives the display domain when omitted, stores the normalized sources with that exact draft revision, and copies them into the authorization provenance snapshot. A later draft revision cannot rewrite the sources shown for an earlier outcome. Unsupported factual or technical advice stays unavailable for approval until credible support is supplied.

Send target_key and native_target_ids at the top level. Native platform IDs take priority over URL text. Bombyx derives and owns the stored identity: normalized platform-native IDs first, then the exact normalized target URL, then a validated connector target key, and finally source-system plus external-key only where no stable target is available. Tracking URL parameters and fragments do not create a new target. Different comments on the same thread or video remain distinct.

For an unsuppressed stable target, a later discovery with a different connector, source system, or external key updates the same live opportunity instead of creating a duplicate. The source identifiers remain ingestion aliases, not a second target identity.

For child targets, provide the complete native tuple. Reddit and Facebook comments need both post_id and comment_id; Instagram comments need media_id and comment_id; YouTube comments need video_id and comment_id. A partial tuple never collapses a child target to its thread, post, media, or video: Bombyx uses complete URL identity when available instead.

When a member closes an actionable target, Bombyx records an organization-scoped exact-target tombstone. A later upsert for that exact target, including one with a new external key, source system, or draft wording, returns HTTP 201 with suppressed: true, suppression_reason: "member_closed", and suppression_scope: "exact_public_target". It creates, reopens, updates, and revises nothing. It writes only a compact suppression audit containing connector and target identifiers, never the rejected draft content. This also applies to MCP upserts.

Before proposing work, a connector may make the read-only GET /app/api/tool-connectors/v1/operator/opportunities/suppression check with platform plus the available URL, native IDs, target key, or source identifiers. Send native IDs as OpenAPI deepObject query fields, for example native_target_ids[post_id]=...&native_target_ids[comment_id]=.... It returns suppressed, a safe reason, and scope. The MCP equivalent is bombyx_operator_opportunity_suppression_check.

Only an authenticated organization member can lift a tombstone from History using Allow suggestions again. Lifting records an audit event but never reopens old work: a later discovery creates fresh live work or dedupes into a fresh live record. Historical backfill is dry-run by default: php artisan operator:backfill-suppressions --json; use --apply only after reviewing unresolved entries. The command reads only historical operator_opportunity_dismissed audits, excludes final executions, consolidates exact-target duplicates, validates a recorded target identity against the current row, and reports rows it cannot safely identify or prove were not reused.

Legacy Reddit review rows are audited separately with php artisan operator:backfill-review-context --json. This command is dry-run by default and can be limited with --organization=ID; --apply demotes only active Reddit reply/comment rows whose stored context is missing, malformed, v1-only, or not review-eligible. It never guesses a target URL, source, author, or citation. Applied demotions remain in the same organization history with an audit event and an agent-owned refresh action. A later trustworthy v2 context and draft-bound sources can upsert the same exact target and restore it to Review without rewriting closed history.

Close and upsert lock the same organization-and-target tombstone key. Production verification should include a MySQL concurrent first-close versus upsert probe, plus a double-close probe, because portable SQLite tests cannot model MySQL gap-lock timing. On a non-production MySQL environment only, run php scripts/operator-opportunity-suppression-mysql-concurrency-check.php --confirm-dev-fixture. The probe refuses production and non-MySQL connections, creates only a clearly marked fake fixture, uses separate worker processes, and removes the fixture before exit.

For non-Reddit targets, the backward-compatible v1 context remains supported: send metadata.review_context.version: 1, visibility: public, kind: reply_target or comment_target, and nonblank original public text. Include parent_title whenever available. Optional author, handle, captured_at, and content_hash help reviewers and are preserved in the approval snapshot. Missing or private context is not allowed in Review; legacy rows without it stay in In progress as Context needed before review.

New Reddit reply and comment targets require review context v2. The context is public-only and thread-first, with the exact target permalink, source post, bounded related public comments, and a server-derived aggregate context_hash. The top-level platform must match review_context.platform, and top-level canonical_url must identify the same exact target as target_comment.permalink; a mismatch remains agent-owned and cannot be approved. Reddit items can enter Review only when value_add is distinct_contribution; duplicates, context gaps, and no_worthwhile_response remain agent-owned in In progress.

{
"review_context": {
"version": 2,
"visibility": "public",
"conversation_id": "reddit-thread-123",
"thread_id": "t3_123",
"platform": "reddit",
"community": "r/example",
"community_url": "https://www.reddit.com/r/example/",
"source_post": {
"id": "t3_123",
"title": "How do I repair this paint chip?",
"author": "u/example",
"body": "The public post body.",
"permalink": "https://www.reddit.com/r/example/comments/123/question/",
"created_at": "2026-07-12T18:00:00Z"
},
"media": [{"url": "https://cdn.example.test/photo.jpg", "alt_text": "Paint chip close-up"}],
"target_comment": {
"id": "t1_456",
"parent_id": "t3_123",
"author": "u/example",
"handle": "u/example",
"text": "What should I use before touching this up?",
"permalink": "https://www.reddit.com/r/example/comments/123/question/456/",
"created_at": "2026-07-12T18:05:00Z",
"is_op": true
},
"related_comments_subset": true,
"related_comments": [
{
"id": "t1_455",
"parent_id": "t3_123",
"relationship": "direct_reply",
"author": "u/helpful-detailer",
"handle": "u/helpful-detailer",
"text": "Clean the chip before applying any product.",
"permalink": "https://www.reddit.com/r/example/comments/123/question/455/",
"created_at": "2026-07-12T18:04:00Z",
"is_op": false,
"thread_order": 0
}
],
"captured_at": "2026-07-12T18:10:00Z",
"value_add": "distinct_contribution",
"what_this_adds": "A concise preparation step not covered by the thread."
}
}

v2 requires at least one of conversation_id or thread_id, plus source-post ID, title, author, permalink, and timestamp. Source body and community URL are optional when the public source does not provide them. The target and every related comment require an author or handle, exact permalink, timestamp, and is_op; related comments also use one of parent_path, direct_reply, or relevant. Set related_comments_subset: true only when the supplied related comments are a selected subset of the source thread. An optional thread_order integer preserves a connector-supplied source-thread order, while depth or indentation preserves supplied nesting only. v2 accepts HTTPS web URLs only, limits media to 8 items and related comments to 25, and normalizes text before hashing. The returned and approved snapshot contains the canonical context_hash; clients must not supply or rely on a client-computed hash. Approval freezes the complete normalized context, semantic value-add decision, and proposed public identity.

Use blockers only for actual hard gates. Put cautions and risk notes in metadata, and use next_action, next_action_due_at, and lifecycle_reason_code for agent-owned pending work.

At the start of each run:

  1. List approvals with GET /app/api/tool-connectors/v1/operator/authorizations.
  2. Choose an item with authorization.claimable: true, then claim it with POST .../{id}/claim and a unique run key for that execution attempt. claimable is only a read-only lifecycle eligibility candidate: it is true for an active approval or a retry whose scheduled time has arrived. It is false after a claim and for completed, expired, revoked, or not-yet-due work. It does not reserve the action. POST /claim remains authoritative and can still reject because another live claim exists, the executor state changed, a concurrent update won the race, or the operator’s downstream local-capacity gate is unavailable. Keep the returned attempt_id. Repeating a still-active claim with the same run key is idempotent. One executor can hold only one live claimed authorization at a time across the organization, so finish or release it before claiming the next approval.
  3. Re-read the live target and apply the organization and platform rules.
  4. Call GET .../{id}?run_key={run_key}&attempt_id={attempt_id} immediately before submission. This confirms the approving member still belongs to the organization, the authorization remains unrevoked, and the exact 15-minute claim remains active.
  5. Perform one action and verify the visible result.
  6. Record completion with the same run_key and attempt_id, final content, the public URL, visible identity, and verification data.

Every revalidate, completion, block, verification-required, and release request is bound to the exact run and attempt returned by claim. An expired lease is never auto-reclaimed because the interrupted run may have submitted before it stopped. Reconcile that exact attempt by releasing it with proof that no public mutation occurred, or use verification-required when the result is uncertain.

Ordinary GET preflight calls are idempotent and do not consume recovery. claimable chooses pre-claim work. execution_allowed is separate and becomes true only for the exact live claimed attempt. Require authorization.execution_allowed: true before any public mutation. A stale lease, expired authorization, or removed approver returns the exact claimed attempt read-only with reconciliation_required: true and a reason; the GET never destroys the release or quarantine path. If the agent corrects one recoverable problem inside the same attempt, call the same GET once with recovery=true. A second explicit recovery is rejected, but another ordinary immediate-before-submit preflight remains available. If the connector loses the completion response, call the same exact-run and attempt GET again. A completed authorization returns its immutable nested authorization.attempt, including final content, public URL, visible identity, verification, and performed_at, without performing another public action.

Treat the authorization’s top-level decision_key, platform, target_url, target_key, native_target_ids, action_type, and provenance_snapshot as the immutable execution contract. Bombyx captures them when a member approves the action. The nested opportunity remains current review context and can change on a later sync, so it must not replace the authorization snapshot during execution or ledger reconciliation.

provenance_snapshot also contains the target key and native target IDs recorded at approval. A released attempt is immutable. Schedule a future retry and claim it with a new run key, which creates a new attempt while preserving the prior evidence.

The authorization uses open_discretion. The draft is guidance, not an exact-text requirement. The connector may adapt wording, but organization knowledge, identity, platform rules, and other hard gates remain mandatory.

  • Use /block only for one of the documented hard-gate codes: captcha_or_security_challenge, account_restricted, identity_mismatch, phone_verification_required, password_reset_required, passkey_or_security_key_required, identity_document_required, paid_access_required, nonroutine_terms_required, or policy_prohibits_action.
  • Use /verification-required when submission may have occurred but the result cannot be confirmed. Never retry that action blindly.
  • Use /release for recoverable work only after checking the public target and composer. Send a supported reason_code, a future next_retry_at, and structured proof_no_mutation with confirmed: true. The retry remains approved and appears as agent work. After 24 hours of recoverable attempts, automatic retries stop in needs_attention without mislabeling the item as blocked. Omit next_retry_at only when reconciling an authorization that already expired, was revoked, or whose approver left the organization; those releases preserve expired or revoked and cannot retry.
{
"run_key": "round-2026-07-14-am-youtube-1",
"attempt_id": 42,
"reason_code": "composer_unavailable",
"reason": "The visible reply composer did not load.",
"next_retry_at": "2026-07-14T23:00:00Z",
"proof_no_mutation": {
"confirmed": true,
"checked_at": "2026-07-14T22:42:00Z",
"method": "Public thread and composer reread"
},
"reconciliation": {"public_result": "absent"}
}

An authorization expires seven days after approval and permits one successful public action.

When a member selects Try again, Bombyx creates one organization-scoped retry request for the opportunity’s assigned drafting connector. Older deployments may still have source_refresh retries that were created when a member edited a source-required technical draft. Their normal lifecycle is queued to claimed to completed; transient releases use retry_wait, while needs_guidance, cancelled, and superseded are explicit stop paths. The connector calls POST /app/api/tool-connectors/v1/operator/draft-retries/claim with a unique run_key. The oldest due request is atomically claimed for 15 minutes and includes request_kind, the target, public review context, feedback, complete revision history, current draft, active coaching, and evidence references.

Complete the exact claim with POST .../operator/draft-retries/{id}/complete, draft_content, an adaptation_summary, feedback_analysis, evidence references, and an operator-draft-retry-review-v1 receipt. A normal member_feedback retry requires materially different wording. A legacy source_refresh retry must preserve the member’s wording exactly and cannot rewrite it to match older source material. A confirmed member correction can satisfy the source requirement for its recorded context. The analysis records whether the rejected draft contained a factual, identity, policy, or safety failure. The receipt confirms composition, independent review, every required check, and no more than one rewrite. Completion creates a new tool draft for member review only. It never authorizes or publishes anything.

Release the exact claim at POST .../operator/draft-retries/{id}/release with transient, needs_guidance, or superseded. A needs_guidance release requires one plain-language question in reason. A target-context change supersedes the request. Three transient releases stop automatic retry and become needs_guidance.

Process due retry claims before discovering new opportunities in each existing operator run. Compose first from the supplied Brain evidence, active coaching, target context, and complete reviewer feedback. Then independently review the candidate against the reviewer note, platform policy, unsupported-claim rules, organization voice, identity, factual accuracy, and safety. One bounded rewrite is allowed. Unsupported or conflicting factual guidance must produce one plain-language needs_guidance question instead of a guessed reply.

Saving a revision does not train the Assistant. A revision becomes eligible learning evidence only after an authenticated organization member approves that exact user-authored revision, or approves a tool draft produced by this retry workflow. An ordinary first-pass tool draft approval remains ineligible.

Use GET /app/api/tool-connectors/v1/operator/learning/evidence to list unprocessed evidence. Each item includes the original tool draft, the saved revision chain, the exact approved revision, and a fingerprint. Intermediate saves are context only. They are not positive examples.

Use GET /app/api/tool-connectors/v1/operator/learning/context to retrieve currently active coaching for the connector’s Assistant.

After a governed analysis pass, submit one idempotent batch to POST /app/api/tool-connectors/v1/operator/learning/apply. The batch must disposition every evidence item and may contain:

  • reusable writing guidance, stored as active Assistant Coaching;
  • exact member corrections, stored immediately as contextual authoritative guidance with the wording preserved, plus possible broader factual generalizations stored as pending Brain proposals for member review.

The apply endpoint rechecks that every item still refers to an approved user revision, rejects stale fingerprints, and prevents a reviewed revision from being learned twice. Reusing a batch_key with the same payload is safe. Reusing it with different content is rejected.

Broad coaching requires corroboration. Global guidance must cite at least three independently approved revisions, while answer-mode or topic guidance must cite at least two. A single approved revision can create only narrowly matched question-pattern or single-example coaching with an explicit question or trigger.