Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
Developer Handover Pack
Brand Asset Management Automation
[YourCompany.com] · Marketing Department · Prepared by FullSpec · [Today's Date]
This document is the complete technical reference for the FullSpec team building the Brand Asset Management automation. It covers the current-state process map with bottleneck flags, full agent specifications with IO contracts, an end-to-end data flow trace, and the recommended build stack. Everything the build requires, from field names to fallback behaviour to credential configuration, is documented here. The process owner and marketing team do not need to act on this document directly; it is the internal build specification that the FullSpec team works from.
01Process snapshot
02Agent specifications
Reads each uploaded file's name and available metadata from the designated Google Drive staging folder, determines the asset type (logo, template, photography, font, brand guideline, or other) and likely campaign association based on filename tokens, assigns a sequential version number by checking existing assets in Brandfolder under the same base name, and checks for duplicate or superseded assets already in the library. Outputs a structured tag object and a duplicate flag consumed by every downstream agent. This agent is the entry point for the entire flow and its tag accuracy directly determines the integrity of the library. Estimated build time: 10 hours. Complexity: Moderate.
// Input GoogleDrive.file.id // unique file identifier GoogleDrive.file.name // raw filename as uploaded GoogleDrive.file.mimeType // used to confirm asset type category GoogleDrive.file.createdTime // ISO 8601 timestamp GoogleDrive.file.owners[0].emailAddress // uploader identity // Output tag.asset_type // enum: logo | template | photography | font | guideline | other tag.campaign // string inferred from filename token or 'unassigned' tag.version_number // integer, incremented from latest Brandfolder match tag.base_name // normalised filename without version suffix tag.duplicate_flag // boolean: true if an identical base_name already active in Brandfolder tag.superseded_asset_id // Brandfolder asset ID of prior version, null if none found tag.uploader_email // passed from Google Drive owner field tag.created_timestamp // ISO 8601, forwarded from Drive metadata
- Google Drive API v3 is required. The staging folder must be a single, known folder ID stored as an environment variable (STAGING_FOLDER_ID). The agent must not watch subfolders unless explicitly configured.
- Brandfolder API key must have read access to all collections for the duplicate lookup. Write access is not required at this stage and must not be granted to this agent's credential.
- Asset type classification uses filename token matching first (e.g. '_logo_', '_template_', '_photo_'). If no token matches, mimeType is used as a fallback (image/png or image/jpeg defaults to 'photography'; application/pdf defaults to 'guideline'). A catch-all 'other' category is set if neither rule resolves.
- Version numbering: query Brandfolder for assets where custom_field.base_name matches the normalised incoming name. Take the highest existing version integer and increment by 1. If no match, set version to 1.
- Duplicate flag is set to true only when an active (not archived) Brandfolder asset matches the base_name AND the same version integer. A higher version number on an incoming file is not treated as a duplicate.
- Normalised base_name strips trailing version suffixes (_v1, _v2, -final, -FINAL, _FINAL2, etc.) before matching. A regex list of known suffixes must be maintained and confirmed with the marketing team before go-live.
- The Brandfolder collection structure must be mapped and confirmed before this agent is built. The agent assigns a target_collection_id from a lookup table keyed on tag.asset_type. This table is a build-time config file, not dynamic.
- If the Google Drive file cannot be read (permissions error or unsupported mime type), the agent must write an error record to the error log table and send a Slack alert to the ops channel. It must not proceed to downstream agents.
Receives the structured tag output from the Asset Intake and Classification Agent and manages the full brand review lifecycle. Creates a formatted Asana task assigned to the brand lead (James Okoro), sends a Slack approval request to the configured brand-review channel, monitors the Asana task for a status change, fires a single reminder at the 24-hour mark if no decision is recorded, and routes the outcome: approved assets proceed to the Library Update and Archive Agent; rejected assets generate a new Asana task assigned back to the designer with the brand lead's feedback notes attached. This agent is the human-gate manager and must never auto-approve an asset regardless of timeout. Estimated build time: 14 hours. Complexity: Moderate.
// Input tag.asset_type // used in Asana task title and Slack message body tag.campaign // included in task description tag.version_number // included in task title tag.base_name // used as asset reference label tag.duplicate_flag // if true, a warning line is added to the Asana task description tag.uploader_email // used to identify designer for rejection routing GoogleDrive.file.id // linked in Asana task and Slack message // Output (on approval) approval.decision // enum: approved | rejected approval.asana_task_id // Asana task ID for audit trail approval.decided_at // ISO 8601 timestamp of status change approval.brand_lead_notes // string, may be empty on approval approval.file_drive_id // forwarded from input for Library Update Agent approval.tag // full tag object forwarded to Library Update Agent // On rejection rejection.asana_task_id // new designer revision task ID rejection.designer_email // resolved from tag.uploader_email rejection.feedback_notes // copied from brand lead's Asana comment rejection.original_file_id // for designer reference
- Asana personal access token or OAuth 2.0 client credentials must be scoped to: tasks:write, tasks:read, projects:read. The target project ID (brand-review project) must be stored as an environment variable (ASANA_BRAND_REVIEW_PROJECT_ID) and confirmed before build.
- Asana task naming convention: '[asset_type] | [base_name] v[version_number] — Brand Review'. This format is fixed and must not vary, as downstream filtering relies on it.
- Slack approval message must be sent to a dedicated channel (e.g. #brand-approvals). Channel ID must be stored as an environment variable (SLACK_BRAND_APPROVALS_CHANNEL_ID). The message must include the Google Drive file link, asset type, version number, and a note if duplicate_flag is true.
- The 24-hour reminder is a single fire; the agent must not send multiple reminders. The reminder is sent via Slack to the same channel and also adds a comment to the Asana task. If no decision is recorded after 48 hours, an escalation alert is sent to the marketing manager (Claire Morton) via Slack DM. The agent still does not auto-approve.
- Decision detection: the agent polls the Asana task status at a configurable interval (default 15 minutes). Alternatively, an Asana webhook can be registered for the project to push task-completion events. Webhook approach is preferred to reduce polling overhead.
- Rejection routing: on rejection, a new Asana task is created in the designer's personal task list (or a designated revision project, to be confirmed) with the brand lead's comment copied into the description. A Slack DM is sent to the designer's Slack user ID (resolved from uploader_email via Slack users.lookupByEmail). The rejected flow then idles until the designer re-uploads to the staging folder, which re-triggers the Intake Agent.
- The agent must not hold state between cycles in memory. All intermediate state (task ID, timestamps, reminder-sent flag) must be persisted to the workflow run record or an external state store before any wait step.
Receives the approval record from the Approval Workflow Agent and executes three parallel write operations: uploads the approved, tagged asset to the correct Brandfolder collection with full metadata and permissions; moves the superseded version (if one exists) to the archive folder in Google Drive and marks it as archived in Brandfolder; and appends a new row to the asset register Google Sheet with all version, approval, and location data. On completion it posts a standardised new-asset notification to the brand-assets Slack channel. This agent closes the loop and produces the authoritative library state. Estimated build time: 12 hours. Complexity: Moderate.
// Input approval.decision // must equal 'approved'; agent aborts on any other value approval.tag // full tag object from Intake Agent approval.file_drive_id // Google Drive file ID of the approved asset approval.asana_task_id // written to asset register for audit trail approval.decided_at // written to asset register approval.brand_lead_notes // written to Brandfolder asset description if non-empty tag.superseded_asset_id // Brandfolder asset ID to archive; null if version 1 tag.target_collection_id // Brandfolder collection destination // Output brandfolder.new_asset_id // Brandfolder asset ID of uploaded file brandfolder.asset_url // direct public or permissioned Brandfolder link brandfolder.archived_asset_id // ID of the prior version now marked archived; null if none gdrive.archived_file_id // Google Drive file ID after move to archive folder sheets.row_index // row number written to asset register slack.notification_ts // Slack message timestamp for the new-asset alert
- Brandfolder API key must have write access scoped to: assets:create, assets:update, attachments:create, collections:read. A separate read-only key is used by the Intake Agent. Keys must not be shared between agents.
- Brandfolder upload payload must include: name (tag.base_name + ' v' + tag.version_number), description (from brand_lead_notes or a default template string if empty), custom fields (asset_type, campaign, version_number, approval_date, asana_task_id), and the target collection_id from the lookup table.
- Permissions on the uploaded Brandfolder asset must match the collection's default permission tier. Do not set asset-level overrides unless the asset_type is 'guideline', which requires restricted access (internal team only). Permission tier mapping must be confirmed before build.
- Archiving in Brandfolder: if tag.superseded_asset_id is not null, PATCH the existing asset to set custom_field.status = 'archived' and remove it from active collection search using the Brandfolder API tags endpoint. Do not delete it.
- Google Drive archive move: the superseded file (identified by tag.superseded_asset_id resolved back to a Drive file ID via a stored mapping) is moved using the Drive API files.update with addParents set to the ARCHIVE_FOLDER_ID environment variable and removeParents set to the STAGING_FOLDER_ID. The archive folder ID must be confirmed before build.
- Google Sheets asset register: the sheet ID and target tab name must be stored as environment variables (ASSET_REGISTER_SHEET_ID, ASSET_REGISTER_TAB_NAME). Columns written per row: filename, asset_type, campaign, version_number, brandfolder_url, approval_date, asana_task_id, brand_lead_notes, uploader_email. Column order is fixed; any schema change to the sheet requires a corresponding update to this agent.
- Slack new-asset notification is posted to the #brand-assets channel (SLACK_BRAND_ASSETS_CHANNEL_ID). Message must include: asset name, type, version number, Brandfolder direct link, and the name of the brand lead who approved it. The notification is the final step; if it fails, the failure must be logged but must not roll back the Brandfolder upload or sheet write.
- All three write operations (Brandfolder upload, Drive archive, Sheets append) must be treated as independent. A failure in one must log an error and alert via Slack to the ops channel without blocking the other two.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────
// TRIGGER: File added to Google Drive staging folder
// ─────────────────────────────────────────────────────────────────
GoogleDrive.watch(folderId: STAGING_FOLDER_ID)
-> event.file.id : string // e.g. '1aBcDeFgHiJkLmNoPqRs'
-> event.file.name : string // e.g. 'acme_logo_horizontal_final.png'
-> event.file.mimeType : string // e.g. 'image/png'
-> event.file.createdTime : ISO8601 // e.g. '2025-05-23T09:14:00Z'
-> event.file.owners[0].emailAddress : string // uploader
// ─────────────────────────────────────────────────────────────────
// AGENT 1: Asset Intake and Classification Agent
// ─────────────────────────────────────────────────────────────────
INPUT:
file.id, file.name, file.mimeType, file.createdTime, file.owners[0].emailAddress
STEP 1.1 normalise_filename(file.name)
-> base_name : string // strips _v1/_FINAL/etc suffixes
-> inferred_asset_type : enum // logo|template|photography|font|guideline|other
-> inferred_campaign : string // token match or 'unassigned'
STEP 1.2 Brandfolder.assets.list(filter: custom_fields.base_name == base_name, status: active)
-> existing_assets[] : array // may be empty
-> max_existing_version : integer // max version_number in results, 0 if empty
-> superseded_asset_id : string // ID of max_existing_version asset, null if none
STEP 1.3 resolve_version(max_existing_version)
-> version_number : integer // max_existing_version + 1
-> duplicate_flag : boolean // true if incoming file matches active base_name at same version
OUTPUT (tag object forwarded to Agent 2):
tag.asset_type, tag.campaign, tag.version_number, tag.base_name,
tag.duplicate_flag, tag.superseded_asset_id, tag.uploader_email,
tag.created_timestamp, tag.target_collection_id, file.id
// ─────────────────────────────────────────────────────────────────
// AGENT 2: Approval Workflow Agent
// ─────────────────────────────────────────────────────────────────
INPUT:
tag.* (full tag object), file.id
STEP 2.1 Asana.tasks.create()
body.name : '[asset_type] | [base_name] v[version_number] — Brand Review'
body.projects : [ASANA_BRAND_REVIEW_PROJECT_ID]
body.assignee : ASANA_BRAND_LEAD_USER_ID // James Okoro
body.due_on : now + 48h
body.notes : 'Asset type: [asset_type]\nCampaign: [campaign]\nDrive link: [file_url]\n[DUPLICATE WARNING if flag=true]'
-> asana_task_id : string
STEP 2.2 Slack.chat.postMessage()
channel : SLACK_BRAND_APPROVALS_CHANNEL_ID
text : 'New asset for review: [base_name] v[version_number] ([asset_type])\n[Drive link]\nPlease approve or reject in Asana: [task_url]'
-> slack_message_ts : string
STEP 2.3 WAIT: poll Asana.tasks.get(asana_task_id) every 15 min
OR receive Asana webhook event: task.completed | task.stories.comment_added
STEP 2.4 IF elapsed >= 24h AND no decision:
Slack.chat.postMessage(channel: SLACK_BRAND_APPROVALS_CHANNEL_ID, text: 'REMINDER: ...')
Asana.tasks.addComment(asana_task_id, '24-hour review reminder sent.')
STEP 2.5 IF elapsed >= 48h AND no decision:
Slack.conversations.open(users: SLACK_MARKETING_MANAGER_USER_ID)
Slack.chat.postMessage(channel: dm_channel, text: 'ESCALATION: [base_name] v[version_number] awaiting approval for 48h.')
STEP 2.6 decision = Asana.tasks.get(asana_task_id).completed_by + custom_field.decision
-> approval.decision : enum // approved | rejected
-> approval.decided_at : ISO8601
-> approval.brand_lead_notes : string // from Asana task comment
STEP 2.7 IF rejected:
Asana.tasks.create() -> designer revision task (assignee: designer, notes: feedback)
Slack.users.lookupByEmail(tag.uploader_email) -> designer_slack_user_id
Slack.chat.postMessage(channel: dm to designer, text: 'Asset rejected. Feedback: [notes]. Please revise and re-upload.')
-> FLOW ENDS (designer re-upload restarts from trigger)
OUTPUT (approval record forwarded to Agent 3 on approval only):
approval.decision, approval.asana_task_id, approval.decided_at,
approval.brand_lead_notes, approval.file_drive_id (= file.id),
approval.tag (full tag object)
// ─────────────────────────────────────────────────────────────────
// AGENT 3: Library Update and Archive Agent
// ─────────────────────────────────────────────────────────────────
INPUT:
approval.* (full approval record including nested tag.*)
STEP 3.1 GoogleDrive.files.get(approval.file_drive_id) -> binary stream
Brandfolder.assets.create()
body.name : '[base_name] v[version_number]'
body.description : brand_lead_notes OR 'Approved [asset_type] — [campaign]'
body.collection_id : tag.target_collection_id
body.custom_fields : {asset_type, campaign, version_number, approval_date: decided_at, asana_task_id}
body.attachments : [drive file binary]
-> brandfolder.new_asset_id : string
-> brandfolder.asset_url : string
STEP 3.2 IF tag.superseded_asset_id != null:
Brandfolder.assets.update(tag.superseded_asset_id)
body.custom_fields.status : 'archived'
Brandfolder.assets.removeTag(tag.superseded_asset_id, tag: 'active')
-> brandfolder.archived_asset_id : string
Resolve superseded Drive file ID from internal mapping store
GoogleDrive.files.update(superseded_drive_file_id)
body.addParents : ARCHIVE_FOLDER_ID
body.removeParents : STAGING_FOLDER_ID
-> gdrive.archived_file_id : string
STEP 3.3 GoogleSheets.values.append(ASSET_REGISTER_SHEET_ID, ASSET_REGISTER_TAB_NAME)
row: [base_name, asset_type, campaign, version_number, brandfolder.asset_url,
decided_at, asana_task_id, brand_lead_notes, uploader_email]
-> sheets.row_index : integer
STEP 3.4 Slack.chat.postMessage()
channel : SLACK_BRAND_ASSETS_CHANNEL_ID
text : 'New asset live in Brandfolder: [base_name] v[version_number] ([asset_type])\nApproved by James Okoro\n[brandfolder.asset_url]'
-> slack.notification_ts : string
// ─────────────────────────────────────────────────────────────────
// FLOW COMPLETE: Asset is live in Brandfolder, prior version archived,
// register updated, team notified.
// ─────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Brand Asset Management.