API Reference
Exporting controls
Controls are exported using the controls_paged query. Each control includes its category, effectiveness status, linked risks, assets, and framework references.
Basic query
query ExportControls($first: Int, $after: String, $filter: ControlFilter) {
controls_paged(first: $first, after: $after, filter: $filter) {
edges {
node {
id
sequenceId
customId
name
description
category
notBefore
expires
archived
risks {
id
sequenceId
name
treatment
}
assets {
id
name
}
assignments {
assignmentType
user {
name
email
}
}
controlStatus {
progress {
effectiveness
closedTasks
totalTasks
}
issues {
open
closed
}
}
attributes {
key
value
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Variables to fetch the first page:
{ "first": 50, "after": null, "filter": {} }
Key fields
sequenceId— the human-readable number shown in the UI (e.g. C-15)customId— optional custom identifier (e.g. ISO 27001 control reference)category— the control category, e.g.ACCESS_CONTROL,DATA_SECURITY_AND_PRIVACYcontrolStatus.progress.effectiveness—UNDETERMINED,INEFFECTIVE, orEFFECTIVEcontrolStatus.progress.closedTasks/totalTasks— task completion ratio
Complete export script (Python)
import requests
import csv
GRAPHQL_URL = "https://portal.tidalcontrol.com/graphql"
QUERY = """
query ExportControls($first: Int, $after: String, $filter: ControlFilter) {
controls_paged(first: $first, after: $after, filter: $filter) {
edges {
node {
id
sequenceId
customId
name
description
category
notBefore
expires
archived
risks { id sequenceId name treatment }
assets { id name }
assignments { assignmentType user { name email } }
controlStatus {
progress { effectiveness closedTasks totalTasks }
issues { open closed }
}
}
}
pageInfo { hasNextPage endCursor }
}
}
"""
def graphql(query, variables, access_token):
r = requests.post(
GRAPHQL_URL,
json={"query": query, "variables": variables},
headers={"Authorization": f"Bearer {access_token}"},
)
r.raise_for_status()
result = r.json()
if "errors" in result:
raise RuntimeError(result["errors"])
return result["data"]
def export_controls(access_token, include_archived=False):
controls = []
cursor = None
control_filter = {"archived": include_archived} if include_archived else {}
while True:
data = graphql(
QUERY,
{"first": 50, "after": cursor, "filter": control_filter},
access_token,
)
page = data["controls_paged"]
controls.extend(edge["node"] for edge in page["edges"])
if not page["pageInfo"]["hasNextPage"]:
break
cursor = page["pageInfo"]["endCursor"]
return controls
def to_csv(controls, output_path):
if not controls:
print("No controls found.")
return
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
"ID", "Sequence ID", "Custom ID", "Name", "Description",
"Category", "Not Before", "Expires", "Archived",
"Effectiveness", "Closed Tasks", "Total Tasks",
"Open Issues", "Closed Issues",
"Linked Risks", "Linked Assets", "Owners",
])
for c in controls:
progress = c["controlStatus"]["progress"]
issues = c["controlStatus"]["issues"]
owners = [
a["user"]["email"]
for a in c["assignments"]
if a["assignmentType"] == "OWNER"
]
writer.writerow([
c["id"],
c["sequenceId"],
c.get("customId", ""),
c["name"],
c.get("description", ""),
c.get("category", ""),
c.get("notBefore", ""),
c.get("expires", ""),
c["archived"],
progress["effectiveness"],
progress["closedTasks"],
progress["totalTasks"],
issues["open"],
issues["closed"],
", ".join(r["name"] for r in c["risks"]),
", ".join(a["name"] for a in c["assets"]),
", ".join(owners),
])
print(f"Exported {len(controls)} controls to {output_path}")
if __name__ == "__main__":
ACCESS_TOKEN = "eyJhbGci..."
controls = export_controls(ACCESS_TOKEN)
to_csv(controls, "controls_export.csv")
Filtering examples
Only effective controls:
# Filter in Python after fetching (effectiveness is in controlStatus, not a top-level filter field)
effective = [c for c in controls if c["controlStatus"]["progress"]["effectiveness"] == "EFFECTIVE"]
Controls in a specific category:
{ "filter": { "categories": ["ACCESS_CONTROL"] } }
Controls linked to a specific framework:
{ "filter": { "frameworks": ["framework-uuid-here"] } }
Controls with open issues:
with_issues = [c for c in controls if c["controlStatus"]["issues"]["open"] > 0]
Count only
query {
controls_count(filter: { archived: false })
}
- Previous
- Exporting risks
- Next
- Exporting assets