Gensail allows you to requeue jobs that were automatically skipped due to processing filters. This enables you to adjust filter settings and reprocess calls without re-ingesting them.
When a job is created, it enters the processing pipeline. Before transcription begins, the worker validates the job against processing filters configured for your workspace. If a job doesn't meet the filter criteria (e.g., call duration is too short), it's marked as skipped instead of being processed.
Skipped jobs remain in the system and can be requeued at any time. When requeued, the job is re-validated against current filter settings, allowing you to:
- Lower filter thresholds and reprocess previously skipped calls
- Retry calls after fixing configuration issues
- Batch requeue calls after bulk filter changes
┌─────────────────────────────────────────┐
│ │
▼ │
queued ───► transcribing ───► analyzing ───► publishing ───► completed
│
│ (filter validation fails)
▼
skipped ───────────────────────────────────────────────────────────►
│ │
└──────────── requeue (API) ───────────────────┘Processing filters are evaluated by the worker before transcription. Currently supported filters:
| Filter | Config Path | Default | Description |
|---|---|---|---|
min_duration_seconds | processing.filters.min_duration_seconds | 30 | Minimum call duration in seconds |
Filters are inherited through the configuration hierarchy:
- Platform defaults (30 seconds)
- Partner configuration
- Organization configuration
- Workspace configuration
PATCH /api/v1/jobs/{job_id}/status{
"status": "queued"
}{
"job_id": "12345",
"previous_status": "skipped",
"new_status": "queued",
"message": "Job requeued successfully (requeue #1). Job will be re-validated against current processing filters."
}Requires an API key with access to the job's workspace:
- Workspace-scoped API keys can requeue jobs in their workspace
- Organization-scoped API keys can requeue jobs in any workspace within the organization
- Partner-scoped API keys can requeue jobs in any workspace under the partner
| Status | Reason |
|---|---|
400 Bad Request | Invalid status (only queued supported) |
400 Bad Request | Job is not in skipped status |
403 Forbidden | No access to job's workspace |
404 Not Found | Job not found |
import httpx
async def requeue_job(api_key: str, job_id: int) -> dict:
"""Requeue a skipped job."""
async with httpx.AsyncClient() as client:
response = await client.patch(
f"https://analytics-api.gensail.com/api/v1/jobs/{job_id}/status",
headers={"Authorization": f"Bearer {api_key}"},
json={"status": "queued"},
)
response.raise_for_status()
return response.json()
# Usage
result = await requeue_job("gsk_xxx", 12345)
print(f"Job {result['job_id']} requeued: {result['message']}")curl -X PATCH "https://analytics-api.gensail.com/api/v1/jobs/12345/status" \
-H "Authorization: Bearer gsk_xxx" \
-H "Content-Type: application/json" \
-d '{"status": "queued"}'To requeue multiple skipped jobs, first list them using the jobs endpoint:
# Get all skipped jobs for a workspace
curl "https://analytics-api.gensail.com/api/v1/jobs?status=skipped&workspace_id=xxx" \
-H "Authorization: Bearer gsk_xxx"Then iterate and requeue each job:
async def bulk_requeue_skipped(api_key: str, workspace_id: str):
"""Requeue all skipped jobs for a workspace."""
async with httpx.AsyncClient() as client:
# List skipped jobs
response = await client.get(
"https://analytics-api.gensail.com/api/v1/jobs",
headers={"Authorization": f"Bearer {api_key}"},
params={"status": "skipped", "workspace_id": workspace_id},
)
response.raise_for_status()
jobs = response.json()["jobs"]
# Requeue each job
results = []
for job in jobs:
result = await requeue_job(api_key, int(job["job_id"]))
results.append(result)
return resultsWhen a job is requeued, the following metadata is recorded in source_metadata:
| Field | Description |
|---|---|
requeue_count | Number of times this job has been requeued |
last_requeued_at | ISO 8601 timestamp of most recent requeue |
last_requeued_by | Scope of API key used for requeue (workspace/organization/partner) |
previous_skipped_reason | The skip reason before requeue |
previous_skipped_at | When the job was previously skipped |
POST /api/v1/jobs/{job_id}/retry with mode=rerun creates a child job (parent_job_id set) marked source_metadata.supersedes_parent: true. A superseding child is a reprocessing of the same call, not a new call:
GET /api/v1/calls/listingshows ONE row — the parent (originalreceived_at, original source ids) — carrying the newest completed grading run across the family. The child never appears as its own row. Searching by the child's job id resolves to the parent row.GET /api/v1/jobs/{root_id}/runsincludes the children's runs, and the top-level transcription fields come from the effective family job — the member owning the selected grading run (newest completeddental_grading_*across the family, the exact/calls/listingrule; families without a completed grading run fall back to the newest completed run of any algorithm) — with expliciteffective_job_id+root_job_statusprovenance. Requesting a child id directly returns only that child's runs (audit access)./stats/feedback-trendsand/stats/duration-distributioncount the call once; a failed root recovered by a successful superseding child counts as processed. Operational job metrics (/stats,/stats/daily,/stats/by-organization) and billing (/stats/trends) deliberately count physical jobs, children included.- Family depth is one level. Rerunning a job that is itself a child attaches the new job to the family ROOT (no grandchildren); the immediate source is recorded in
source_metadata.rerun_parent_job_id. - Non-superseding children — on-demand jobs from
POST /jobs/{id}/run-algorithms(supersedes_parent: false) and legacy children created before this feature — keep their own listing row and never replace the original call's result.
When a job is skipped, the skipped_reason field indicates why:
| Reason | Source | Description |
|---|---|---|
skip_all_on_ingest | Poller | All calls are skipped when skip_all_on_ingest: true is configured |
rate_limit | Poller | Rate limits (max_calls_per_workspace or max_total_calls) were exceeded |
duration_below_minimum:{actual}<{min} | Worker | Call duration is shorter than configured minimum |
When skip_all_on_ingest: true is configured in the CallRail integration settings, ALL calls are created with status skipped regardless of rate limits. This is useful for:
- Loading all historical calls for manual review
- Bulk ingestion without automatic processing
- Allowing selective processing via the API
Configuration:
{
"integrations": {
"callrail": {
"skip_all_on_ingest": true
}
}
}Workflow:
- Poller ingests all calls as
skippedwith reasonskip_all_on_ingest - Review calls in your application or via
GET /jobs?status=skipped - Selectively queue jobs for processing via
PATCH /jobs/{job_id}/status - Worker picks up queued jobs and applies processing filters
When a requeued job is picked up by a worker:
- The job is re-validated against current processing filters
- If filters pass, processing continues to transcription
- If filters still fail, the job is marked as
skippedagain
This means:
- If you haven't changed filter settings, the job will be skipped again
- To process previously skipped calls, lower the filter threshold first
- Requeue count tracks how many times a job has been requeued
- Adjust filters before bulk requeue: Lower
min_duration_secondsbefore requeuing to avoid immediate re-skip - Monitor requeue counts: High requeue counts may indicate configuration issues
- Use bulk operations carefully: Rate limiting applies to API requests
- Check job status after requeue: Jobs may be skipped again if filters haven't changed