Amazon S3
The Amazon S3 Connector lists objects in a bucket, reads their contents, writes and replaces objects, changes their metadata and storage class, deletes them, copies them, and restores archived objects. It works the same way as the Google Cloud Storage Connector: buckets are where files arrive from other systems and where a Program leaves its exports.
Sources
s3.objects— objects in the bucket:resource,version(the ETag, or theversion_idon a versioned bucket),key,size,content_type,etag,version_id,last_modified,storage_class,metadata(user metadata as a JSON object). The key iskey. Filter by prefix to list one "folder".LIST_FILES(s3, glob => r'inbound/*.pdf')is the same listing by path, withresource,versionandpath.s3.contents— the content of objects:resource,version,key,media_typeandcontent, acontent_ref. Join it onresourceandversionfrom the listing; the Connector reads only the versions the join names. For typed rows from CSV, Parquet or JSON-lines objects, use the Local files Connector's table sources pointed at ans3://path.
version identifies the bytes that were listed: on a versioned bucket a
replaced object is a different version_id, and on an unversioned bucket a
changed ETag is read as a conflict rather than as the file that was listed.
Types
| S3 field | ActQL | Notes |
|---|---|---|
key, content_type, etag, version_id, storage_class | string | The ETag is quoted as S3 returns it. |
size | int64 | Bytes. |
last_modified | timestamp | UTC, second precision. |
metadata | json | x-amz-meta-* entries as an object of strings. |
content | content_ref | With media_type from content_type. |
Effects and operations
Record Effects write by key:
- update —
EFFECT UPDATE s3.objects ID key SET (content_type = …, storage_class = …, metadata = …)rewrites metadata or storage class. S3 has no metadata-only update, so the Connector copies the object onto itself withMetadataDirective: REPLACE, conditioned on the ETag seen in the Preview; the content is unchanged. - delete —
EFFECT DELETE s3.objects ID keydeletes the object; on a versioned bucket, theversion_idit was previewed at.
Content:
- upload —
EFFECT STORE s3.objects SET (key = …, content = content [, content_type = …, storage_class = …, overwrite = TRUE])creates an object from the row'scontent(acontent_reffromSERIALIZEor from acontentssource). Withoutoverwritethe write is sent withIf-None-Match: *; with it, withIf-Matchon the ETag seen in the Preview.
Operations:
- copy —
EFFECT COMMAND s3.copy TARGET key ID key INPUT (destination = 'other-bucket/path/file.pdf')copies an object within or across buckets the account can write. - restore —
EFFECT COMMAND s3.restore TARGET key ID key INPUT (days = 7, tier = 'Standard' | 'Bulk' | 'Expedited')requests a temporary restore of a Glacier or Deep Archive object.
Setup
"s3": {
"connector": "s3",
"bucket": "acme-exports",
"region": "eu-west-1",
"prefix": "actql/",
"credentials": {
"access_key_id": {"env": "AWS_ACCESS_KEY_ID"},
"secret_access_key": {"env": "AWS_SECRET_ACCESS_KEY"}
}
}An IAM role assumed by the Host can be used instead of keys by omitting
credentials. The identity needs s3:ListBucket on the bucket,
s3:GetObject (and s3:GetObjectVersion on versioned buckets) to read,
s3:PutObject to upload and rewrite, s3:DeleteObject /
s3:DeleteObjectVersion to delete, and s3:RestoreObject to restore; a copy
also needs read on the source and s3:PutObject on the destination. prefix
limits the connection to part of the bucket, and a Program cannot name a key
outside it. An endpoint field selects an S3-compatible service. On ActQL
Cloud the keys live in the Cloud's secret store, or the Cloud assumes a role
you grant it; the connection fields are the same.
Capabilities and pushdown
| Operation | Pushed down when |
|---|---|
| Projection | Always; listing returns the metadata fields, and a Program that only needs keys gets a key-only listing. |
WHERE on s3.objects | The predicate is a prefix on key (LIKE 'inbound/%') — S3 lists by prefix. Other metadata predicates run on the Host over the listing. |
ORDER BY key, LIMIT | Listing order is by key; the limit becomes the page size. |
| Content reads | Per object, by version, after the listing has selected it. A byte-range read is used when a Program needs only part of a CSV header. |
Aggregation, JOIN | Never; they run on the Host. |
Preview and reconciliation
Preview. Metadata updates and deletes are full_diff: the Connector
reads the object's metadata (HEAD), records its ETag and version_id, and
the Preview shows current and new values; the request carries If-Match, so
an object changed in between makes the item failed with conflict, never a
silent overwrite. Uploads are targeted: the Preview shows the key, media
type, byte count and content identity (the SHA-256 the Connector sends as the
checksum) that would be written, and whether an existing object would be
replaced. Copies preview source and destination; restores preview the object,
the tier and the number of days.
Reconciliation. Each item is bound to the key, the expected checksum and,
for multipart uploads, the upload id. Every upload, copy and metadata rewrite
carries the request's marker in user metadata (x-amz-meta-actql-request);
a lost reply is reconciled by HEAD on the destination for that marker,
and a multipart upload by its recorded upload id. A matching checksum or
matching fields without the marker are not evidence. Deletes and restores
leave no marker S3 keeps, so a lost reply to either stays unknown.
An in-progress multipart upload is listed by its upload id and either
completed from its recorded parts or aborted — never completed blindly. An object with
the right key but a different checksum is not evidence; the item stays
unknown and nothing is resent. Uploads, copies, updates and deletes end
applied on confirmation; a restore ends accepted and stays so while
Glacier works, with explain --run and status showing the expected
completion.
Limits and cost
- Listing is paged 1,000 keys at a time, within the Program's row limit.
- Contents are bounded by the Program's byte limit; a large object is refused rather than partially read. Reads verify the ETag or checksum.
- Objects above 8 MiB are uploaded as multipart uploads; a single object may be up to 5 TiB. Copies of objects above 5 GiB are multipart copies.
- S3 charges per request class (LIST and GET, PUT and COPY), for storage by class, for restores by tier, and for data transfer out of the region; the Connector's requests are ordinary billed operations.
Example
Files that arrived in the last hour:
FROM s3.objects
|> WHERE key LIKE 'inbound/%' AND last_modified >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
|> SELECT key, size, last_modified
|> ORDER BY last_modified DESC;Write a daily export of open deals:
FROM hubspot.deals
|> WHERE stage NOT IN ('closedwon', 'closedlost')
|> SELECT id, dealname, amount, stage
|> SERIALIZE FORMAT CSV ORDER BY id
OPTIONS (name = CONCAT('open-deals-', FORMAT_DATE('%Y%m%d', CURRENT_DATE()), '.csv'),
header = TRUE, null_marker = '')
|> EFFECT STORE s3.objects SET (key = 'exports/' || name, content = content);Copy the PDFs that arrived into an archive bucket (a second S3 connection,
archive), unchanged:
FROM LIST_FILES(s3, glob => r'inbound/*.pdf') AS f
|> JOIN s3.contents AS c ON f.resource = c.resource AND f.version = c.version
|> EFFECT STORE archive.objects SET (key = 'pdf/' || f.path, content = c.content);Bring last year's archived statements back for a week:
FROM s3.objects
|> WHERE key LIKE 'statements/2025/%' AND storage_class = 'DEEP_ARCHIVE'
|> EFFECT COMMAND s3.restore TARGET key ID key INPUT (days = 7, tier = 'Bulk');Source: actql/actql/docs/connectors/s3.md at d904416674e4 · Edit on GitHub