← All ivy-nodes
IVYXSTUDIO · IVY NODE
S

Social: the copywriter's request as the model's context

ivy.node.copy-brief · v0.1.2

ivyx✓

Puts a content request together as one text for the model's turn: the brand, its voice and audience, the pillar, the format, the accounts to write for with each platform's length, the brief. No model, no network.

#social#copywriter#ivyx-social

Inputs

FieldTypeDescription
briefstringNot declared
clientstringNot declared
tonestringNot declared
audiencestringNot declared
pillarstringNot declared
formatstringNot declared
titlestringNot declared
accountsarrayThe accounts to write for: key, platform.

Outputs

FieldTypeDescription
output_schemaobjectThe JSON schema the model's answer is held to: variants keyed by every account asked for.
contextrequiredstringThe request as one text.
accountsarrayNot declared
countrequiredintegerNot declared

Source

python

# Input preparation
inp = __ivy_ctx__["nodes"][__ivy_node_id__]["input"]

# Compute
# Shared by every social node (built into each by build.mjs): dates, blocks, merging.
import datetime, json, re, statistics

def day(s):
    s = str(s or "")[:10]
    return s if re.fullmatch(r"\d{4}-\d{2}-\d{2}", s) else None

def now_iso():
    return datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"

def ms_of(s):
    if s is None or s == "":
        return None
    if isinstance(s, (int, float)):
        return int(s * 1000) if s < 1e12 else int(s)
    t = str(s).replace("Z", "+00:00")
    t = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", t)
    try:
        return int(datetime.datetime.fromisoformat(t).timestamp() * 1000)
    except ValueError:
        return None

def rows_of(block):
    """A {fields, rows} block as dicts."""
    if not isinstance(block, dict) or not block.get("fields"):
        return []
    f = block["fields"]
    return [dict(zip(f, r)) for r in block.get("rows") or []]

def block_of(dicts, first):
    fields = list(first)
    for d in dicts:
        for k in d:
            if k not in fields:
                fields.append(k)
    return {"fields": fields, "rows": [[d.get(k) for k in fields] for d in dicts]}

def listed(v):
    return [x for x in (v if isinstance(v, list) else [v] if v else []) if x is not None]

def text_of(v, limit=4000):
    s = " ".join(str(v).split()) if isinstance(v, (str, int, float)) else ""
    return s[:limit]


class CopyBrief:
    "Social: the copywriter's request as the model's context"

    def run(self, inp):
        out = {}
        # The copywriter's request as one text for the model's turn: the brand, its voice and audience, the pillar, the format, the accounts to write for, the brief.
        accounts = [a for a in listed(inp.get("accounts")) if isinstance(a, dict) and a.get("key")]
        LIMITS = {"instagram": 2200, "linkedin": 3000, "x": 280, "youtube": 5000, "facebook": 63206, "tiktok": 2200}
        lines = [f"Brand: {text_of(inp.get('client')) or 'the brand'}."]
        if inp.get("tone"):
            lines.append(f"Voice: {text_of(inp['tone'])}.")
        if inp.get("audience"):
            lines.append(f"Audience: {text_of(inp['audience'])}.")
        if inp.get("pillar"):
            lines.append(f"Pillar: {text_of(inp['pillar'])}.")
        if inp.get("format"):
            lines.append(f"Format: {text_of(inp['format'])}.")
        if inp.get("title"):
            lines.append(f"Working title: {text_of(inp['title'])}.")
        lines.append("Accounts to write for, one variant each: " + "; ".join(f"{a['key']} ({a.get('platform') or '?'}, at most {LIMITS.get(a.get('platform'), 2000)} characters{', hashtags go in the first comment' if a.get('platform') == 'instagram' else ''})" for a in accounts) + ".")
        lines.append("Brief: " + (text_of(inp.get("brief")) or "(none)"))
        out["context"] = "\n".join(lines)
        out["accounts"] = [a["key"] for a in accounts]
        out["count"] = len(accounts)
        # The schema the model's answer is held to: one variant per account, by its key. A list let a 9B model
        # answer for one account of two in a run of five, and the draft went out short (2026-10-25). The hashtags are
        # one string: a list of bare words came back unquoted in one run of twelve, which is not JSON.
        VARIANT = {"type": "object", "required": ["text"], "properties": {"text": {"type": "string", "minLength": 1}, "hashtags": {"type": "string"}, "first_comment": {"type": "string"}}}
        out["output_schema"] = {"type": "object", "required": ["variants"], "properties": {"variants": {"type": "object", "required": [a["key"] for a in accounts], "properties": {a["key"]: VARIANT for a in accounts}}}}
        return out


result = CopyBrief().run(inp)

# Output collection (the runner reads it here)
out = __ivy_ctx__["nodes"][__ivy_node_id__]["output"]
out.update(result)

Tests

Requires: python:3.9

  • names

    Two accounts are listed with their platform's length, Instagram's hashtags sent to the first comment.

  • empty

    An empty request still says what is missing rather than inventing a brand.