Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions airbyte-integrations/connectors/source-ashby/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The Ashby API uses `.list` endpoints with cursor-based pagination. The `applicat
| Stream | Volume Tier | Relationship | Cursor Field | API Incremental Support | Current Status | Notes |
|---|---|---|---|---|---|---|
| applications | large | top-level parent | none | created_at_only | deferred_no_api_support | Has `createdAfter` in body; mutable resource (status changes). Verify if `updatedAfter` is supported. |
| application_history | large | substream of applications | none | none | full_refresh_only | No date filter or `syncToken`; one request per application; ~23 hours for ~108,100 applications at ~1.31 req/s. |
| archive_reasons | small | top-level parent | none | none | deferred_no_api_support | Config-style lookup |
| candidate_tags | small | top-level parent | none | none | deferred_no_api_support | Config-style lookup |
| candidates | large | top-level parent | none | none | deferred_no_api_support | No documented date filter on `.list`. High volume. |
Expand Down
209 changes: 209 additions & 0 deletions airbyte-integrations/connectors/source-ashby/manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
version: 4.3.0
type: DeclarativeSource
api_budget:
type: HTTPAPIBudget
policies:
- type: MovingWindowCallRatePolicy
rates:
- limit: 100
interval: PT1M
matchers:
- type: HttpRequestRegexMatcher
url_path_pattern: /application\.listHistory
check:
type: CheckStream
stream_names:
Expand Down Expand Up @@ -1359,6 +1369,7 @@ streams:
fields:
- path:
- application_id
type: AddedFieldDefinition
value: "{{ stream_partition.application_id }}"
schema_loader:
type: InlineSchemaLoader
Expand Down Expand Up @@ -1391,6 +1402,204 @@ streams:
type:
- "null"
- string
- type: DeclarativeStream
name: application_history
primary_key:
- id
retriever:
type: SimpleRetriever
requester:
type: HttpRequester
url_base: https://api.ashbyhq.com
authenticator:
type: BasicHttpAuthenticator
username: "{{ config['api_key'] }}"
password: "{{ config['api_key'] }}"
path: /application.listHistory
http_method: POST
request_body_json:
applicationId: "{{ stream_partition.application_id }}"
error_handler:
type: DefaultErrorHandler
response_filters:
- type: HttpResponseFilter
action: RATE_LIMITED
http_codes:
- 429
- type: HttpResponseFilter
action: RETRY
http_codes:
- 500
- 502
- 503
- 504
- type: HttpResponseFilter
action: IGNORE
predicate: >-
{{ response.get('success', true) == false and
response.get('errorInfo', {}).get('code') == 'application_not_found' }}
error_message: >-
application.listHistory skipped this application's history
(Ashby request ID:
{{ response.get('errorInfo', {}).get('requestId', 'unavailable') }}).
- type: HttpResponseFilter
action: FAIL
predicate: "{{ response.get('success', true) == false }}"
error_message: >-
application.listHistory failed with code
{{ response.get('errorInfo', {}).get('code', 'unknown') }}:
{{ response.get('errorInfo', {}).get('message') or
(response.get('errors') and response.get('errors')[0].get('message')) or
'Unknown error' }}.
record_selector:
type: RecordSelector
extractor:
type: DpathExtractor
field_path:
- results
paginator:
type: DefaultPaginator
page_token_option:
type: RequestOption
inject_into: body_json
field_name: cursor
page_size_option:
type: RequestOption
inject_into: body_json
field_name: limit
pagination_strategy:
type: CursorPagination
page_size: 100
cursor_value: "{{ response.nextCursor }}"
stop_condition: "{{ not response.moreDataAvailable }}"
partition_router:
type: SubstreamPartitionRouter
parent_stream_configs:
- type: ParentStreamConfig
stream:
type: DeclarativeStream
name: applications_for_history
primary_key:
- id
retriever:
type: SimpleRetriever
requester:
type: HttpRequester
url_base: https://api.ashbyhq.com
authenticator:
type: BasicHttpAuthenticator
username: "{{ config['api_key'] }}"
password: "{{ config['api_key'] }}"
path: /application.list
http_method: POST
request_body_json:
createdAfter: "{{ timestamp(config['start_date']) * 1000 }}"
record_selector:
type: RecordSelector
extractor:
type: DpathExtractor
field_path:
- results
paginator:
type: DefaultPaginator
page_token_option:
type: RequestOption
inject_into: body_json
field_name: cursor
page_size_option:
type: RequestOption
inject_into: body_json
field_name: limit
Comment on lines +1509 to +1512

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch on the inconsistency, but it points the other way: limit is correct and the existing per_page usages are the latent bug. Ashby's ApplicationListRequest is additionalProperties: false with syncToken, cursor, limit, createdAfter, createdBefore, status, jobId, and expand — there is no per_page parameter anywhere in the API. So the streams injecting per_page are most likely having their page size silently ignored today and falling back to Ashby's default page size, which is exactly why this connector's measured throughput is as low as it is.

I'm not fixing that here on purpose. Correcting per_page to limit across the existing streams changes the request shape and page count for every stream in the connector, which deserves its own PR with its own version bump and its own regression run rather than riding along inside a new-stream PR — especially with #84274 open against this same manifest. The new stream uses the documented parameter so it's right from the start, and I've flagged the cleanup separately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚫 Not fixing here — filed as #84394 instead. Disposition marker plus the tracking issue for the per_page cleanup, which I couldn't add to the reply above (the API rejects edits to inline review comments from this account).

pagination_strategy:
type: CursorPagination
page_size: 100
cursor_value: "{{ response.nextCursor }}"
stop_condition: "{{ not response.moreDataAvailable }}"
schema_loader:
type: InlineSchemaLoader
schema:
type: object
$schema: http://json-schema.org/draft-07/schema#
additionalProperties: true
properties:
id:
type: string
parent_key: id
partition_field: application_id
extra_fields:
- - status
- - createdAt
transformations:
- type: AddFields
fields:
- type: AddedFieldDefinition
path:
- application_id
value: "{{ stream_partition.application_id }}"
- type: AddedFieldDefinition
path:
- application_status
value: "{{ stream_slice.extra_fields['status'] }}"
- type: AddedFieldDefinition
path:
- application_created_at
value: "{{ stream_slice.extra_fields['createdAt'] }}"
schema_loader:
type: InlineSchemaLoader
schema:
type: object
$schema: http://json-schema.org/draft-07/schema#
additionalProperties: true
properties:
id:
type: string
format: uuid
Comment on lines +1554 to +1556

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deliberate, and I'd like to keep it non-nullable. id is this stream's declared primary key, and Ashby's ApplicationHistory response schema marks it required, so a null there would mean an unkeyable record rather than a value we should quietly accept. The nullable-union style you're citing on applications.properties.id is a real inconsistency, but it's the pre-existing side of it: that stream declares primary_key: [id] while typing id as ["null", "string"], which is the combination that actually risks trouble. Tightening applications is out of scope here — this PR deliberately touches no existing stream, since #84274 is open against this same manifest.

Worth noting the typing has no effect on whether a null gets through: additionalProperties: true plus the default transformer means a null id would still be emitted, just flagged against the schema. So this is about declaring intent accurately, and "the primary key is always present" is the accurate intent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚫 Not fixing — disposition marker for the reply above, which I can't edit in place (the API rejects edits to inline review comments from this account).

application_id:
type:
- "null"
- string
application_status:
type:
- "null"
- string
application_created_at:
type:
- "null"
- string
format: date-time
stageId:
type:
- "null"
- string
format: uuid
title:
type:
- "null"
- string
enteredStageAt:
type:
- "null"
- string
format: date-time
leftStageAt:
type:
- "null"
- string
format: date-time
stageNumber:
type:
- "null"
- integer
actorId:
type:
- "null"
- string
allowedActions:
type:
- "null"
- array
items:
type: string
spec:
type: Spec
connection_specification:
Expand Down
2 changes: 1 addition & 1 deletion airbyte-integrations/connectors/source-ashby/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ data:
connectorSubtype: api
connectorType: source
definitionId: 4e8c9fa0-3634-499b-b948-11581b5c3efa
dockerImageTag: 0.3.8
dockerImageTag: 0.4.0
dockerRepository: airbyte/source-ashby
githubIssueLabel: source-ashby
icon: ashby.svg
Expand Down
8 changes: 6 additions & 2 deletions docs/integrations/sources/ashby.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Your API key must have read permissions enabled for the modules that correspond

| Ashby permission module | Streams |
| :--- | :--- |
| Candidates | `applications`, `application_criteria_evaluations`, `candidates` |
| Candidates | `applications`, `application_criteria_evaluations`, `application_history`, `candidates` |
| Interviews | `interviews`, `interview_stages`, `interview_schedules` |
| Jobs | `jobs`, `job_postings` |
| Hiring Process | `archive_reasons`, `candidate_tags`, `custom_fields`, `feedback_form_definitions`, `sources` |
Expand All @@ -32,7 +32,7 @@ The `application_criteria_evaluations` stream requires the AI Application Review
2. Generate an API key following the [Ashby authentication guide](https://developers.ashbyhq.com/reference/authentication). Grant the API key read permissions for the modules listed in the prerequisites. At minimum, you must enable the **Organization** read permission (required for the connection check) plus read permissions for any additional modules whose streams you want to sync.
3. In Airbyte, create a new Ashby source.
4. Enter your **API key**.
5. Enter a **Start date** in `YYYY-MM-DDTHH:MM:SSZ` format. The connector sends this date as the `createdAfter` filter on the `applications` and `interview_schedules` streams, so records created before it aren't replicated. The date also limits `application_criteria_evaluations`, because that stream reads the same filtered application list to decide which applications to request evaluations for. All other streams ignore the start date and always return everything the API exposes.
5. Enter a **Start date** in `YYYY-MM-DDTHH:MM:SSZ` format. The connector sends this date as the `createdAfter` filter on the `applications` and `interview_schedules` streams, so records created before it aren't replicated. The date also limits `application_criteria_evaluations` and `application_history`, because those streams read the same filtered application list to decide which applications to request child records for. All other streams ignore the start date and always return everything the API exposes.

## Supported sync modes

Expand All @@ -49,6 +49,7 @@ This source syncs the following streams:

- [applications](https://developers.ashbyhq.com/reference/applicationlist)
- [application_criteria_evaluations](https://developers.ashbyhq.com/reference/applicationlistcriteriaevaluations) (substream of applications)
- [application_history](https://developers.ashbyhq.com/reference/applicationlisthistory) (substream of applications)
- [archive_reasons](https://developers.ashbyhq.com/reference/archivereasonlist)
- [candidate_tags](https://developers.ashbyhq.com/reference/candidatetaglist)
- [candidates](https://developers.ashbyhq.com/reference/candidatelist)
Expand All @@ -67,6 +68,8 @@ This source syncs the following streams:

The `application_criteria_evaluations` stream is a substream of `applications`. The connector requests evaluations only for applications whose current interview stage has the type `PreInterviewScreen` and whose status is neither `Archived` nor `Hired`, so it doesn't cover every application in your account. Each record carries an `application_id` field copied from the parent application, which is how you join evaluations back to `applications`. This stream has no primary key, and the connector doesn't paginate the evaluations endpoint, so only the first page of evaluations is synced for each application.

The `application_history` stream is a full-refresh substream of `applications` that returns interview stage entry and exit events for every application selected by the start date. Join `application_history.application_id` to `applications.id` and `application_history.stageId` to `interview_stages.id` for funnel analysis. Each history sync makes one or more requests per application and does not support incremental sync, so run it on its own connection on a slow schedule. A deleted or inaccessible application is skipped and logged; any other API failure fails the sync. At approximately 1.31 requests per second with 108,100 applications, the application fan-out alone takes roughly 23 hours; pagination adds more requests.

## Performance considerations

Ashby doesn't publish a rate limit for the `.list` endpoints this connector reads, and the connector reads one stream at a time, so syncs are unlikely to be throttled. Ashby's rate limits apply per organization, so an API key shared with other integrations has less headroom.
Expand All @@ -82,6 +85,7 @@ If you use Airbyte Cloud and your organization restricts access to specific IPs,

| Version | Date | Pull Request | Subject |
|:--------| :--------- | :------------------------------------------------------- |:--------------------------------------------|
| 0.4.0 | 2026-08-14 | [84392](https://github.com/airbytehq/airbyte/pull/84392) | Add application history stream |
| 0.3.8 | 2026-08-11 | [84215](https://github.com/airbytehq/airbyte/pull/84215) | Promoted release candidate to GA |
| 0.3.8-rc.5 | 2026-08-11 | [84214](https://github.com/airbytehq/airbyte/pull/84214) | Revert the concurrency work from 0.3.8-rc.1 through 0.3.8-rc.3: remove the API budget, concurrency level, and `num_workers` option. |
| 0.3.8-rc.4 | 2026-08-11 | [83816](https://github.com/airbytehq/airbyte/pull/83816) | Add missing application, candidate, and source fields to the declared schemas, and remove duplicated unreferenced manifest blocks. |
Expand Down
Loading