building-syncs
Build Celigo syncs (Celigo Data Ingestion) -- managed pipelines that replicate many tables from a source application into a data warehouse, with a historical backfill, incremental runs and schema-drift handling. Covers sync versus flow, the sync integration, datasets, destinations, the enable and license gates, runs and resyncs, and sync jobs, errors and events. Use when replicating application data into Snowflake, Google BigQuery, Microsoft SQL Server or NetSuite Analytics Warehouse, or when building, running or troubleshooting a sync.
How do I install this agent skill?
npx skills add https://github.com/celigo/ai --skill building-syncsIs this agent skill safe to install?
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
Building Syncs
A sync (Celigo Data Ingestion) replicates data from one source application into a data warehouse. It pairs a source connection with a destination (a warehouse connection plus a database and schema), and its datasets choose which tables or objects it copies, which columns, and how each table loads. The first run backfills history from historicDateTime; later runs are incremental. Runs happen on a cron schedule or on demand, and each one leaves a read-only sync job with per-table children, run errors, and events for schema drift.
A sync is not a flow. It has no steps, mappings, hooks, filters or routers, it lives in its own kind of integration, and its runs and errors are read through sync jobs, not flow jobs or flow errors.
How to Run the Steps
This skill names each step by what it reads or changes; the exact calls depend on how you reach the account. Read the one reference that matches your tools -- both follow the step numbers of How to Build a Sync below, then the headings of Sync Operations:
- The
celigoCLI (celigo syncs,celigo datasets,celigo sync-jobs) -- references/cli.md - Celigo MCP tools -- references/mcp.md. The deployed Celigo MCP server has no sync, dataset or sync-job tools: build and operate syncs with the CLI, the REST API, or the Celigo UI.
Listing syncs, reading a connection's catalog and columns, and reading jobs, errors, events and usage only read (a catalog or column read can query the source application). Creating or changing a sync or its datasets writes to the account. Two writes reach outside it: saving a destination connects to the warehouse and creates the schema if it's missing, and enabling a sync starts its schedule -- every run extracts from the source and loads rows into the warehouse, rows that count against the account's Data Ingestion volume.
Sync or Flow?
Decide this first, and say the limit out loud when a request lands on the wrong side -- never quietly build a flow for someone who asked for a sync.
| The request | Build | Why |
|---|---|---|
| Replicate many tables or objects of an application into a warehouse, with a backfill and incremental updates | Sync | The managed case: tables created and kept in step with the source, drift handled per policy |
| Land one export's records in a warehouse table, with no mappings or hooks on the way | Sync with an export-backed dataset | Reuses the export's extraction; the sync manages the table |
| Move records between two applications ("sync customers from Shopify to NetSuite") | Flow | A sync's destination is always a warehouse |
| Push warehouse data back into an application (reverse ETL, activation) | Flow | Syncs only load into a warehouse |
| Reshape, map, filter or branch each record on the way in | Flow, or a sync that lands raw data you transform in the warehouse | A sync has no per-record steps |
| React to changes as they happen (webhooks, real-time events, change data capture) | Flow, with a listener or CDC export | A sync runs on a schedule or on demand, never on an event |
| Load into a warehouse the destinations list doesn't name | Flow with a database import | A sync's destination must be a listed warehouse |
The supported warehouses come from the destinations list (snowflake, bigquery, mssql, nsaw at the time of writing); read it rather than assume it. A source that offers no catalog can still feed a sync through export-backed datasets.
The Sync Model
The sync integration
A sync lives in an integration created with syncs: true. The flag is fixed at creation -- an update ignores it in both directions -- so an existing flow integration can never hold syncs, and creating a sync in one is refused ("An integration is suppose to have either flows or syncs but not both."). The rule is only checked on the sync side: a flow created in a sync integration is accepted, so keep flows out of it yourself.
_integrationIdis permanent. It's required on create, can't be sent on an update, and a sync can't be moved to another integration.- Its connections are registered. Creating a sync, or saving its destination, adds its connections to the integration's
_registeredConnectionIdsand setsmicroServices.workerGroup: "DIWorker"on each connection (asystemchange in the connection's audit log). Deleting the syncs that use a connection clears its worker group. - One warehouse type per integration. All syncs in an integration load into the same destination application ("The destination type for all the syncs under an integration must be same."). A second warehouse type needs its own integration.
- Syncs block the integration's delete. An integration that still holds a sync can't be deleted ("sync
<syncId>must be deleted before you can delete integration<integrationId>").
Sources: catalog datasets and export-backed datasets
Any connection can be a sync's source. What it can offer depends on its catalog -- the list of tables or objects for that connection:
- Catalog (object mode). A non-empty catalog means the source offers its tables directly: Salesforce, NetSuite saved searches, NetSuite through SuiteAnalytics Connect (JDBC), and the HTTP connectors in the sources list. Each catalog entry's
nameis the dataset'sexternalId, exactly as returned -- for an HTTP-connector source it's an opaque id, not the table's label (displayName). - Export-backed datasets. An empty catalog (
datasets: [], answered with 200, not an error) means the connection feeds a sync only through exports: a plain HTTP connection, an HTTP connector the sources list doesn't name, or a connection on a different API or version of a listed connector. The catalog also lists the connection's exports; a dataset withisExport: trueandexternalIdset to an export's_idloads that export's records. The export must run on the sync's source connection.
An empty catalog is a signal about the connection, not about the data: don't retry it or tell the user the source has no tables.
Destinations
The destination block is _connectionId, database, schema, and an optional tablePrefix. The warehouse is the destination connection's own type -- rdbms.type snowflake, bigquery or mssql, or a NetSuite Analytics Warehouse JDBC connection -- and any other connection is refused ("Invalid destination type and please provide the valid destination type"). For BigQuery, database is the GCP project and schema the dataset, case preserved; it must be the rdbms BigQuery connection, not the Google BigQuery HTTP connector.
- Send the block complete, in one save. A partial block is refused in the warehouse's own words -- "SQL Server database is required on sync destination", "BigQuery dataset is required on sync destination" -- except Snowflake, whose partial save reaches the warehouse and fails there.
- The database must exist; the schema is created if it doesn't. So a misspelled schema becomes a new schema in the customer's warehouse rather than an error. Confirm both names with the user.
- Saving reaches the warehouse. The connection has to work first: against an unreachable host the save can run past 30 seconds and store nothing. Fix the connection rather than trying other names.
- SQL Server checks identifiers: start with a letter or underscore; letters, digits,
_or$; at most 128 characters. - A database and schema pair backs one sync. A destination whose
databaseandschemamatch another sync's -- compared without regard to case, even on another host, warehouse type or integration -- is refused with "Duplicate destination host found". Give each sync its own schema. - A sync's warehouse type is fixed once saved ("Destination connection type is not same as the previous destination connection type"). Loading into another kind of warehouse means a new sync, in an integration of that type.
Datasets
One dataset per table or object the sync replicates. All dataset writes go through one bulk PUT of an array:
- An item with
externalIdcreates; an item with_idupdates. Both or neither fails the batch. It isn't an upsert: creating anexternalIdthe sync already has is refused ("... datasets requested to create are already present"), and so is updating an_idfrom another sync. - One bad item fails the whole batch, including the valid items in it. Datasets the batch doesn't name are left as they are. The response has no body -- read the datasets back.
- An update replaces
name,tableNameandexportPropertieswith what the item carries, so an item that leaves one out clears it; an omittedingestionModeordataElementskeeps its saved value. A partial{_id, enable: false}therefore wipes the dataset's table name. Send every item whole: read the dataset, dropexternalIdand the read-only fields (_userId,_syncId,enableAllDataElements,userActionRequired,mismatchSyncConnection), change what you need, send it back. - Nothing is checked against the source at save. An
externalIdor a column name that isn't in the catalog saves without complaint. Take everyexternalIdfrom the catalog and every column name from the column read. - The API saves only what you send. Selecting a few tables doesn't create disabled datasets for the rest of the catalog.
- There is no dataset delete. Set
enable: false; the dataset stays configured and is skipped by runs. - Table names are unique per sync. An exact duplicate is refused at save. A name that differs only in case saves, but enabling a dataset whose table name matches another's, ignoring case, is refused ("Another dataset in this sync resolves to the same destination table").
Per-dataset fields:
| Field | Meaning |
|---|---|
enable | Replicate on each run. true needs an ingestionMode |
ingestionMode | append adds each run's records and keeps prior rows; replace drops and reloads the table each run; merge upserts by primary key and needs an enabled isPrimaryKey column |
tableName | Destination table name; required for export-backed datasets. Omit to let the platform derive one |
exportProperties | Catalog datasets only: type: "delta" (changed records since the last run) or "all", and delta.dateField -- one of the column read's deltaFields (Salesforce derives it). NetSuite saved searches also need netsuite.recordType |
dataElements | Per-column enable, mask, isPrimaryKey, and transformMode (preserve, flatten, normalize) with children for nested fields. Omit to replicate every column; when sent, it replaces the whole selection |
driftPolicy.element | Overrides the sync's column drift policy for this dataset |
mask: true masks a column's values before they load, so the raw values never reach the warehouse; a primary-key column can't be masked, and transformMode is only valid on a field with children.
Schema drift
driftPolicy says what happens when the source changes between runs:
dataset.added-- tables that appear in the source:automateadds and replicates them,ignoreleaves them out until someone enables them.dataset.removedis alwaysautomate.element.added-- new columns:automateorignore. Withautomate,objectAdded(preserveorflatten) andarrayAdded(preserveornormalize) shape new nested fields; either one withignoreis refused.removed,reducedandexpandedare alwaysautomate.
A sync created through the API starts at dataset.added: "ignore" with an empty element policy -- read it back, since a sync built another way may hold automate. With automate, every table the policy adds loads rows that count against the Data Ingestion volume. A full update that omits driftPolicy, or sends an empty dataset block, keeps the stored dataset.added but clears the element settings, so returning to ignore takes an explicit ignore. What the policy applies is logged as Schema Drift events; drift it doesn't automate sets userActionRequired on the dataset, which blocks updates to it until resolved in the UI.
Runs, jobs, errors, and events
A run is queued and returns at once with syncId and flowExecutionGroupId (the run's correlation key). One run at a time per sync. A normal run is incremental over every enabled dataset; a resync drops the delta checkpoint and reloads everything; either can be scoped to some datasets (datasetIds), though a scoped normal run is refused unless the account has partial normal sync.
- Sync jobs -- one parent job per run (
queued,running,canceling, thencompleted,failedorcanceled) withnumExtractedRecords,numLoadedRecords,numErrors,runType, and a per-table family of child jobs. Job records are kept about 30 days (purgeAt). - Run errors -- read-only diagnostics per run:
stage,scope,source,code,message, and the table (_datasetId,tableName,_childSyncJobId). They can't be retried or resolved: fix the cause and run again. A run checks its connections before it extracts, so an unreachable warehouse fails it at stageinitwithdi_ping_failure. - Events --
Schema DriftandData Catalogentries with aseverity, filterable by run (flowExecutionGroupId), table and time; kept for the account's data-retention period.
The Data Ingestion license
Read the license's di block, not a tier table: enabled, volume (monthly loaded rows included with the edition), volumeAddOn (a purchased volume pack), disableOverage, and histLoadEndDate (a pack's historical-load window). The license is checked when a sync is enabled, not when it's created:
- No Data Ingestion entitlement -- enabling is refused (
di_subscription_required). - Without a volume pack, each environment can have one enabled sync -- enabling another is refused ("You have reached your limit of active syncs. Disable an existing sync or purchase a Data ingestion volume pack to enable more.").
- Past the month's volume while overage is disabled, or past the raised cap during a volume pack's historical-load window -- enabling is refused (
di_usage_limit_exceeded).
Usage counts records loaded, per sync, per month, and per environment; reading it needs account-administration permission.
Quick Reference
Lifecycle
| Stage | What it needs | Refused with |
|---|---|---|
| Container | An integration created with syncs: true | -- |
| Draft | _integrationId and source._connectionId (a real connection). The draft is disabled: true, destination: {}, name: "" | "To create sync integration id is required..." / "Source connection is missing in the request" / "...either flows or syncs but not both." |
| Datasets | One bulk PUT; externalId to create, _id to update | See Datasets |
| Destination | _connectionId, database, schema together | Type, partial-block, identifier and uniqueness messages under Destinations |
| Enable | Source, complete destination, historicDateTime; the license | "Sync can be enabled only when source, destinations are properly defined." / the limit messages |
| Run | A saved destination; enabled; nothing queued or running | "The flow is not present in the sync." / "Sync disabled..." / "Another instance for this sync is already queued or running..." |
| Delete | disabled: true | "Cannot delete an enabled sync..." |
Related Skills
- building-flows (What Starts a Flow) -- app-to-app, per-record, event-driven and warehouse-to-app work, where a sync doesn't fit
- configuring-exports (Quick Reference) -- the export behind an export-backed dataset
- configuring-connections (Quick Reference) -- the source connection and the warehouse (
rdbms) connection a destination uses - managing-integrations (Quick Reference) -- integrations, notifications, and the recycle bin
- getting-started (Working on a Live Account) -- confirming the account before the first write
How to Build a Sync
1. Confirm it's a sync, and read the license
Walk Sync or Flow? above with the user's request. Then read the license's di block: enabled must be true, and without a volume pack an environment that already has an enabled sync can't enable another. List the account's syncs to see what's enabled where. Read the supported sources and destinations rather than assuming them.
2. Find or create the sync integration
List the integrations and pick one created with syncs: true whose syncs load into the same kind of warehouse; otherwise create one with syncs: true in the create body. Never try to add the flag to an existing integration -- the update answers 200 and keeps its value.
3. Pick the source connection and read its catalog
Find the source connection (configuring-connections), then read its catalog. A non-empty datasets list is the menu of tables; an empty one means export-backed datasets only -- the same response lists the connection's exports. Agree with the user which tables to replicate.
4. Create the draft sync
Create the sync with _integrationId and source._connectionId, plus a name. It's created disabled, with an empty destination. Read it back and check driftPolicy.
5. Choose the datasets
Read each chosen table's columns, scoped to the draft sync: names, types, constraints, and deltaFields, the columns usable as a delta cursor (for an export-backed dataset, the export's record structure). The column read asks the source application live, so it needs a working connection.
Then build one batch: an item per table with externalId (from the catalog), a name, enable, an ingestionMode, and the dataElements you want (from the column read), or leave dataElements out to take every column. For merge, mark the key columns isPrimaryKey. For delta extraction on a catalog dataset, set exportProperties: {type: "delta", delta: {dateField: <one of deltaFields>}}, or type: "all" when there's no usable cursor. An export-backed item takes isExport: true, externalId set to the export's _id, a tableName, and an explicit dataElements list, and no exportProperties.
Create the datasets disabled (enable: false) while you review them if you prefer; to change one later, send it whole by _id (Datasets above). Read the datasets back after every batch.
6. Set the destination
Ask which warehouse connection, database and schema to use -- never invent them. Check that the connection's type matches the integration's other syncs, that no other sync uses the same database and schema, and that the connection is online. Then save the three together. The save connects to the warehouse and creates the schema if it's missing, so it can take a while; if it fails, fix the connection.
7. Set the schedule, backfill start, and drift policy
historicDateTime-- the earliest record time to backfill from; required before enabling.schedule-- a 6-field cron with seconds ("? 0 */6 * * *") evaluated intimezone(IANA); leave both out for on-demand runs.driftPolicy-- keepdataset.added: "ignore"unless the user wants new source tables replicated automatically; chooseelement.addedthe same way.
Each is a field of the sync, and every update of the sync replaces the whole document: send it complete (step 9 explains why disabled must be in it).
8. Review columns for personal data
Before the first run, when nothing has loaded yet, go through each dataset's columns with the user and set mask: true on personal data -- email, phone, address, birth date, government and tax ids, bank and card numbers, salary, health -- and on custom fields named like them. Masking can't be combined with a primary key.
9. Enable, then run
Enable by sending the sync with disabled: false. Every full update must carry disabled: the PUT replaces the document, and a complete sync whose update leaves disabled out comes back enabled, and an enabled sync runs on its schedule. An incomplete sync is refused; so is one the license doesn't cover.
Run it on demand (or wait for the schedule). The run returns at once; one run at a time. Use a resync, or a run scoped to some datasets, when a table needs reloading -- see Sync Operations.
10. Watch the first run
Poll the run's sync job until it ends, then read its table family for per-table counts, its errors, and the run's events. Compare numExtractedRecords with numLoadedRecords: the gap is records that failed to load. A di_ping_failure at stage init means the run couldn't reach a connection's host -- fix that connection and run again.
Sync Operations
The operations on a sync beyond building it. Each reference lists its calls under these same headings and names the ones its surface lacks.
- Read and write -- list syncs (all, or one integration's), read one, create, replace the whole document, change fields, read its audit log; read, create and update datasets in bulk; read the catalog, columns, sources and destinations
- Run -- a normal run, a resync, or either one scoped to chosen datasets
- Monitor runs -- list a sync's jobs (by created date), read one, read its per-table family, read its errors (all, or one table's), list the latest job of each sync in an integration, read the sync's events
- Cancel -- cancel one job, or every queued and running job of a sync. Cancel is asynchronous: the job shows
canceling, thencanceled - Pause, change, delete -- disable to pause (the configuration is kept); delete a disabled sync for good; its datasets go with it
- Usage and notifications -- records loaded per sync this month, per month against the license, per environment; subscribe a user to a sync's failure notifications
Pre-Submit Checklist
- The request is a sync, not a flow (a listed warehouse destination, no per-record logic, nothing event-driven or warehouse-to-app)
-
license.di.enabledis true, and enabling won't exceed the environment's enabled-sync limit - The integration was created with
syncs: true, and its syncs share this sync's warehouse type - Every
externalIdcame from the catalog and every column name from the column read - Each update item is the whole dataset (
name,tableName,exportProperties,ingestionMode,dataElements), withoutexternalId - Every
mergedataset has an enabled, unmasked primary-key column; table names are unique per sync, ignoring case - Personal-data columns are masked before the first run
- The destination's connection, database and schema were confirmed with the user and are sent together; the database exists; no other sync uses the database and schema
-
driftPolicy.dataset.addedis what the user chose - Every full update of the sync carries
disabledexplicitly, plushistoricDateTime,schedule,timezone,destinationanddriftPolicy - Each write was read back
Gotchas
- A full update can enable the sync. The PUT replaces the document, and a complete sync whose update leaves
disabledout comes backdisabled: false, and an enabled sync runs on its schedule. Senddisabledon every full update. - The PUT clears what it omits.
schedule,timezone, the destination and the element drift settings go when left out;descriptionandhistoricDateTimestay.dataset.addedkeeps its value when omitted, so switching back fromautomateneeds an explicitignore. Read, modify, write the whole document. - A partial dataset update wipes fields. An update item replaces
name,tableNameandexportProperties;{_id, enable: false}leaves the dataset with no table name -- even an export-backed one, whose create requires it. - The datasets PUT isn't an upsert, and it's all or nothing. Re-creating an existing
externalIdfails the batch, and one bad item rejects the valid ones with it. - Selections aren't verified at save. Made-up table and column names save fine; only the catalog and the column read are ground truth.
- A typo in the schema creates a schema. Saving the destination creates a missing schema in the warehouse instead of failing. The database, by contrast, must exist.
- A database and schema pair belongs to one sync. Another sync naming the same pair -- in any case, on any host or warehouse type -- is refused with "Duplicate destination host found".
- One warehouse type per integration, and per sync for good. Mixing types in an integration is refused, and a saved destination can't change type.
- The container can't be converted.
syncs: trueis set at creation only; an update answers 200 and ignores it. And a flow created in a sync integration is accepted -- don't put one there. - Deleting a sync is permanent. It must be disabled first, it never enters the recycle bin, and its datasets go with it. An integration can't be deleted while it holds a sync, and once its syncs have run, the deleted integration can't be purged from the recycle bin while their job records exist (they're kept about 30 days).
- Creating a sync changes its connections. The platform sets
microServices.workerGroup: "DIWorker"on the source and destination connections and registers them on the integration; deleting the syncs that use a connection clears it. Expect thesesystemchanges in the connections' audit logs. - The license bites at enable, not create. Any number of syncs can be built; without a volume pack only one can be enabled per environment.
- Sync errors have no retry. They're read-only diagnostics of a run: fix the cause and run again, scoped to the failed datasets where the account allows it.
- An empty catalog isn't an empty source. It means the connection only supports export-backed datasets.
Common Errors
The platform's message comes first; the code follows where the API publishes one.
| Error | Cause | Fix |
|---|---|---|
422 "An integration is suppose to have either flows or syncs but not both." (integration_not_for_sync) | The sync's integration wasn't created with syncs: true | Create a new integration with syncs: true; an existing one can't be converted |
422 "Source connection is missing in the request" (missing_source_connection) | source._connectionId is missing, or isn't a connection's id | Send a real connection id; an update needs it too |
422 "Sync can be enabled only when source, destinations are properly defined." (sync_not_enabled) | Enabling without a complete destination or historicDateTime | Save the destination and historicDateTime, then enable |
422 "You have reached your limit of active syncs..." (di_sync_limit_exceeded) | The environment already has an enabled sync and the license has no volume pack | Disable the other sync, or the account needs a Data Ingestion volume pack |
422 "Invalid destination type and please provide the valid destination type" (invalid_destination_type) | The destination connection isn't a supported warehouse type | Use a warehouse connection whose type the destinations list names |
422 "SQL Server database is required on sync destination" / "... schema is required ..." (invalid_destination_sync_config) | A partial destination block | Send _connectionId, database and schema together |
422 "SQL Server schema name is invalid: ..." (invalid_destination_sync_config) | The name breaks SQL Server's identifier rule | A letter or underscore first; letters, digits, _ or $; 128 characters at most |
422 "Duplicate destination host found" (duplicate_destination_host) | Another sync, in any integration, uses the same database and schema, ignoring case | Use a schema of this sync's own |
| 422 "The destination type for all the syncs under an integration must be same." | The integration's other syncs load into another kind of warehouse | Put this sync in an integration for that warehouse type |
| 422 "Destination connection type is not same as the previous destination connection type" | Changing a saved destination to another warehouse type | Build a new sync for the other warehouse |
404 "The flow is not present in the sync." (sync_flow_not_present) | Running a draft whose destination was never saved | Save the destination, enable, then run |
422 "Sync disabled. Please enable your sync and try again." (invalid_sync) | Running a disabled sync | Enable it first |
409 "Another instance for this sync is already queued or running..." (sync_job_already_queued) | A run is already queued or running | Wait for it, or cancel it |
400 "Cannot delete an enabled sync. Please disable the sync before deletion." (sync_delete_not_allowed) | Deleting an enabled sync | Disable it, then delete |
400 "For creation externalId should be provide and for update dataset id should be provided but not both." (dataset_externalId_id_required) | An item has both _id and externalId, or neither | Create with externalId only, update with _id only |
| 400 "... datasets requested to create are already present. Kindly provide new datasets" | Creating an externalId the sync already has | Update that dataset by its _id instead |
400 "Connection is different for the sync and the export dataset." (connection_mismatch) | The export behind an export-backed dataset runs on another connection | Use an export on the sync's source connection |
422 "ingestionMode field is required for enabled datasets." (ingestion_mode_required_for_enabled_datasets) | enable: true with no ingestionMode | Set append, replace or merge |
| 422 "You must select a primary key for a merge operation." | merge with no enabled isPrimaryKey column | Mark the key column(s) |
422 "<column> cannot have both isPrimaryKey and mask set as true" | A masked primary key | Unmask the key column, or choose another key |
400 "Table names should be unique..." (duplicate_dataset_table_names) / 422 "Table name - <name> already exists for a datasets." (table_name_already_exists) | Two datasets with one tableName | Give each dataset its own table name |
| 422 "Another dataset in this sync resolves to the same destination table..." | Enabling a dataset whose table name matches another's, ignoring case | Rename one of them |
422 "Drift policy element.added should be automate if element.arrayAdded/objectAdded is present" (drift_policy_invalid_setup) | objectAdded or arrayAdded with element.added: "ignore" | Drop them, or set element.added: "automate" |
Run error di_ping_failure at stage init ("Unknown host: ...") | The run couldn't reach a connection's host | Fix the connection, then run again |
403 "The job <syncJobId> is already in terminal state..." (sync_job_not_cancellable) | Canceling a finished job | Nothing to cancel |
422 "sync <syncId> must be deleted before you can delete integration <integrationId>" | Deleting an integration that holds syncs | Disable and delete its syncs first |
422 "this document is referenced by ParentSyncJob : <syncJobId>" on a recycle-bin purge | The integration's syncs ran, and their job records outlive them | Purge again after the jobs' purgeAt (about 30 days after each run); no route deletes job records |
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/celigo/ai/building-syncs">View building-syncs on skillZs</a>