{% extends "base.html" %} {# The outreach bot's page (spec part 04, hermitcrm/outreach.py). Nothing here is ever sent: you send it yourself, then log it (logging it marks the item sent). #} {% block title %}Outreach{% endblock %} {% block content %} {% from "macros.html" import fix_with_ai, hint, more as row_more %}

Outreach

{% if send_on %}

Your AI CLI researches and drafts first touches; you review each one before anything goes out. {%- call hint('outreach-sending-help', 'How approved messages go out') %} Approved email goes out from here, now or spaced over the working day. LinkedIn is a session where you click Send yourself. Nothing is sent that you did not approve.{% endcall %} How it works · Sending

{% else %}

Your AI CLI researches and drafts first touches; you review each one and send it yourself. Nothing is sent from here. {%- call hint('outreach-sending-help', 'What logging a message does') %} Logging the message on the contact marks its item sent.{% endcall %} How it works

{% endif %} {% if since_line %}

Since last time: {{ since_line }}. Learning

{% endif %} {% if gate %}

The outreach bot will not run yet: {{ gate }}. Settings > Playbook

{% elif ai_problem %}

{{ ai_problem }}

{% endif %} {% if gen %}{% set gc = gen.contact %}

Generate for {{ gc.first_name or gc.name }}

{{ gc.name }}{% if gc.title %}, {{ gc.title }}{% endif %} at {{ gen.company.name }}. Nothing runs until you press Generate; the draft appears under Drafts to review.

{% if gen.open_item %}

{{ gc.first_name or gc.name }} already has a {{ gen.open_item.kind | replace('-', ' ') }} by {{ gen.open_item.channel }} that is {{ gen.open_item.state }}. Open it

{% else %}
scrape, then ai-web automatically if it finds nothing
{% endif %}

Notes the draft will use ({{ gen.notes | length }})

{% if gen.notes %}
    {% for r in gen.notes %}
  1. {{ r.text }} · {{ fmt_date(r.note.date) }} · {% if r.source_url %}{{ r.source_url | replace('https://', '') | truncate(40) }}{% else %}{{ 'company' if r.origin == 'company' else 'you' }}{% endif %}
  2. {% endfor %}
{% else %}

None yet. Research adds what it finds as notes, and anything you write here or on their page counts too.

{% endif %}

Saved as a note when you press Add note (or Enter){% if not gen.open_item %} or Generate{% endif %}, and the draft uses it. {% if gen.company.website %}One thing worth noting may be on their site.{% endif %}

{% endif %}
{% if send_on %}
Email: {% if send.paused %}paused {{ send.paused_by }}{% else %}on{% endif %} · {{ send.sent_today }}/{{ send.cap }} today · {{ send.queued }} queued{% if send.next and not send.paused %} · next {{ send.next.strftime('%a %H:%M') }}{% endif %} · {{ send.days | day_span }} {{ send.window | replace('-', '–') }}
{% if send.missing %}

Sending cannot start: {{ send.missing }} Settings > Sending

{% endif %} {% if send.overdue %}

{{ send.overdue }} queued {{ 'email is' if send.overdue == 1 else 'emails are' }} overdue (the Mac may have been asleep). The next tick plans them again, one at a time.

{% endif %} {% else %}

Email sending is off. Turn it on in Settings > Sending.

{% endif %} {% if li.ok %}

LinkedIn: invites {{ li.invites_today }}/{{ li.invites_per_day }} today · {{ li.invites_week }}/{{ li.invites_per_week }} this week · messages {{ li.messages_today }}/{{ li.messages_per_day }} today · {{ li.queue }} waiting LinkedIn session

{% endif %}
Build a batch
{% if form.left %}Stop Building: {{ form.left }} left{% endif %}
{% if preview_error %}

{{ preview_error }} {{ fix_with_ai(fix_request) }}

{% endif %} {% if preview %}

Preview: {{ preview.picked | length }} would get a draft

{% for note in preview.notes %}

{{ note }} {{ fix_with_ai(note_requests[loop.index0]) }}

{% endfor %}

No AI was run and nothing was written. High-fit companies ({{ preview.fit_field }} ≥ {{ preview.high_fit_threshold }}) never contacted: {{ preview.high_fit_untouched }} of {{ preview.high_fit }}.

{% if preview.picked %}
    {% for c in preview.picked %}
  1. {{ c.who }} · {% if c.fit is not none %}fit {{ c.fit }}{% else %}no fit score{% endif %} · {{ c.approach or 'no approach' }}{% if c.flags %} ({{ c.flags | join('; ') }}){% endif %}
  2. {% endfor %}
{% endif %} {% if preview.over_limit %}

Over this batch's limit: {{ preview.over_limit | map(attribute='who') | join(', ') }}

{% endif %} {% if preview.excluded %}
Not this time ({{ preview.excluded | length }})
{% endif %} {% if preview.duplicates %}

Likely duplicates: {% for a, b, why in preview.duplicates %}{{ a }} / {{ b }} ({{ why }}){% if not loop.last %}; {% endif %}{% endfor %}

{% endif %}
{% endif %} {% if tab == 'drafts' and replies_waiting %}

{{ replies_waiting }} repl{{ 'y' if replies_waiting == 1 else 'ies' }} to interest wait{{ 's' if replies_waiting == 1 }} for an answer.

{% endif %} {% if tab == 'replies' %}

Replies you owe an answer. The draft offers your first step and asks one question: no time slot, link or price. {%- call hint('replies-help', 'Where these replies come from') %} From the follow-up radar. A reply logged with the outcome interested is listed as it is; for the others, Check replies asks your AI CLI once per reply (no web, no tools) and you confirm what it says.{% endcall %}

{% if reply_rows %} {% elif not cards %}

No replies owed. A reply you log on a contact shows up here until you answer it.

{% endif %}
{% endif %} {% if tab == 'follow-ups' %}

Messages of yours with no answer after {{ nudge_days }} days. {%- call hint('followups-help', 'How follow-ups work') %} From the follow-up radar (Settings: followup_nudge_days). A follow-up is short, has no calendar invite and gives an easy way out; the bot does no research for it. A batch makes one per silence and then lets that person go; draft from their row or the Generate panel to write another, on any channel.{% endcall %}

{% if due_rows %} {% elif not cards %}

No follow-ups are due. A message you send that gets no answer shows up here after {{ nudge_days }} days.

{% endif %}
{% endif %} {% if tab == 'ready' and send_on %}
{% if schedule_error %}

{{ schedule_error }}

{% endif %} {% if schedule_preview %}

Proposed times

Spaced over the sending window with a little jitter, under today's cap, one company a day. Nothing is scheduled until you confirm.

{% if schedule_preview.rows %}{% for row in schedule_preview.rows %}{% endfor %}
Goes outToSubject
{% if row.at %}{{ row.at.strftime('%a') }} {{ fmt_date(row.at) }}, {{ row.at.strftime('%H:%M') }}{% else %}no free slot{% endif %}{{ row.who }}{{ row.item.subject }}
{% endif %} {% if schedule_preview.refused %}
{{ schedule_preview.refused | length }} cannot be scheduled
{% endif %}
{% if schedule_preview.rows %}
{% endif %} Cancel
{% elif ready_email %}
{% endif %}
{% endif %} {% if not cards and tab not in ('replies', 'follow-ups') %}

{% if tab == 'drafts' %}No drafts to review. Preview a batch above, then build it.{% else %}Nothing here.{% endif %}

{% endif %} {% macro history(card) %}{% set i = card.item %} {% if i.ai_body %}
AI original{% if card.steps %} and {{ card.steps | length }} tracked edit{{ 's' if card.steps | length != 1 }}{% endif %}{% if i.earlier %}, {{ i.earlier | length }} regenerated away{% endif %}

The AI's original, kept as drafted. Your changes are tracked edits next to it, each with your reason.

{% if i.ai_subject %}

{{ i.ai_subject }}

{% endif %}
{{ i.ai_body }}
{% if i.ai_pitch %}

after they accept

{{ i.ai_pitch }}
{% endif %}
    {% for step in card.steps %}{% set e = step.edit %}
  1. Edit {{ loop.index }} · {{ ui_datetime(e.at) }} · {{ e.words }} word{{ 's' if e.words != 1 }} {% if e.note %}

    {{ e.note }}

    {% else %}

    No explainer.

    {% endif %} {% if step.subject %}

    Subject: {{ diff(step.subject) }}

    {% endif %}
    {{ diff(step.body) }}
    {% if step.pitch %}

    after they accept

    {{ diff(step.pitch) }}
    {% endif %}
  2. {% endfor %}
{% for old in i.earlier %}

Regenerated away ({{ ui_datetime(old.replaced) }}){% if old.why %}: {{ old.why }}{% endif %} · {{ old.edits | length }} tracked edit{{ 's' if old.edits | length != 1 }}

{{ old.ai_body }}
{% endfor %}
{% endif %} {% endmacro %} {% macro diff(parts) %}{% for kind, text in parts %}{% if kind == 'add' %}{{ text }}{% elif kind == 'del' %}{{ text }}{% else %}{{ text }}{% endif %}{% endfor %}{% endmacro %} {% macro about_line(counts) %}{% if counts %}{% set more = counts[0] > counts[1] %}

About you {{ counts[0] }}, about them {{ counts[1] }}{% if more %}: more about you{% endif %}

{% endif %}{% endmacro %} {% macro skip_panel(i) %} {% endmacro %}
{% for card in cards %}{% set i = card.item %}{% set co = card.company %}{% set ct = card.contact %}{% set has_notes = i.kind != 'reply-to-interest' or card.notes.rows %}

{% if ct %}{{ ct.name }}{% else %}{{ i.contact }}{% endif %} {% if ct and ct.title %}{{ ct.title }}{% endif %} {% if ct and ct.linkedin %}LinkedIn ↗{% endif %} {% if ct and ct.email %}{{ ct.email }}{% endif %} {{ i.channel }} {{ i.state }} {% if i.kind == 'reply-to-interest' %}reply to interest{% endif %}

{% if co %}{{ co.name }} · {{ co.stage }}{% else %}{{ i.company }}{% endif %} · {% if card.fit is not none %}fit {{ card.fit }}{% else %}no fit score{% endif %} {% if co and co.website %}· website ↗{% endif %} · {{ i.approach or 'no approach' }}{% if i.language %} · {{ i.language }}{% endif %}

{% if card.followed.count %}
Followed {{ card.followed.count }} lesson{{ 's' if card.followed.count != 1 }} {% if card.followed.texts %}
    {% for t in card.followed.texts %}
  • {{ t }}
  • {% endfor %}
{% endif %} {% if card.followed.texts | length < card.followed.count %}

{{ card.followed.count - card.followed.texts | length }} since retired or reworded.

{% endif %}

These lessons were in the prompt for this draft. Learning

{% endif %} {% if card.modeled %}

Modeled on: {% for m in card.modeled %}{{ m.label }}{% if not loop.last %}, {% endif %}{% endfor %}. Learning

{% endif %} {% if i.flags %}{% endif %} {% for key, value in card.proposals %}
Research found a {{ key }} for an empty field: {{ value }}
{% endfor %} {% if card.reply %}
Their reply, {{ fmt_date(card.reply.date) }}
{% if card.reply.subject %}{{ card.reply.subject }}
{% endif %}{{ card.reply.body }}
{% endif %} {% if has_notes %}{% set nc = card.notes %}
Notes ({{ nc.rows | length }}{% if nc.rows %}, {{ nc.rows | selectattr('used') | list | length }} used{% endif %})

The draft is written from these notes, which also appear on their page and the company timeline.

{% if i.state == 'drafted' and nc.newer %}

{{ nc.newer }} note{{ 's' if nc.newer != 1 }} added since this draft was written. Regenerate to use {{ 'it' if nc.newer == 1 else 'them' }}.

{% endif %}
    {% for r in nc.rows %}
  1. {{ r.text }} · {{ fmt_date(r.note.date) }} · {% if r.source_url %}{{ r.source_url | replace('https://', '') | truncate(40) }}{% else %}{{ 'company' if r.origin == 'company' else 'you' }}{% endif %}{% if r.used %} · used{% endif %} · on the timeline
  2. {% endfor %}
{% if nc.legacy %}

From before notes were shared, saved as notes when you regenerate or mark ready:

    {% for f in nc.legacy %}
  • {{ f.text }} ({{ f.source }})
  • {% endfor %}
{% endif %} {% if i.state == 'drafted' %}
{% endif %} {% if i.norms %}
Norms applied
    {% for n in i.norms %}
  • {{ n }}
  • {% endfor %}
{% endif %}
{% endif %} {% if i.state == 'drafted' %}
{% if i.channel == 'email' %}{% endif %} {% if i.channel != 'email' %} {% endif %} {% if i.checks %}
    {% for c in i.checks %}
  • {{ c }}
  • {% endfor %}
{% endif %} {% if i.critic %}
Critic {{ i.critic_score }}/5

Lowest: {% for name in critic_scores %}{{ name }} {{ i.critic[name] }}{% if not loop.last %}, {% endif %}{% endfor %}{% if i.critic_reason %}. {{ i.critic_reason }}{% endif %}

{% endif %} {{ about_line(about.get(i.id)) }} {% if i.proof_used %}

Proof: {{ i.proof_used | join('; ') }}

{% endif %}
{{ skip_panel(i) }} {{ history(card) }} {% elif i.state == 'ready' %} {% if i.send_note %}

Held back: {{ i.send_note }}

{% endif %} {% if i.channel == 'email' %}

{{ i.subject }}

{% endif %}
{{ i.body }}
{% if i.pitch %}

after they accept

{{ i.pitch }}
{% endif %} {% if i.checks %}{% endif %} {% if i.critic %}
Critic {{ i.critic_score }}/5

Lowest: {% for name in critic_scores %}{{ name }} {{ i.critic[name] }}{% if not loop.last %}, {% endif %}{% endfor %}{% if i.critic_reason %}. {{ i.critic_reason }}{% endif %}

{% endif %} {{ about_line(about.get(i.id)) }}
{% if send_on and i.channel == 'email' %}
{% else %} {% if i.channel == 'email' and ct and ct.email %}Open in mail{% endif %} {% if i.channel == 'linkedin' and ct and ct.linkedin %}Open LinkedIn{% endif %} Log it {% endif %} {% call row_more() %} {% if send_on and i.channel == 'email' %} {% if ct and ct.email %}Open in mail{% endif %} Log it {% endif %}
{% endcall %}
{{ skip_panel(i) }} {{ history(card) }} {% elif i.state in ('queued', 'failed') %} {% if i.channel == 'email' %}

{{ i.subject }}

{% endif %} {% if i.state == 'queued' %}

{% if i.send_at %}Goes out {{ i.send_at.strftime('%a') }} {{ fmt_date(i.send_at) }}, {{ i.send_at.strftime('%H:%M') }}{% else %}No free slot in the next 60 days{% endif %}{% if i.send_error %} last try: {{ i.send_error }}{% endif %}

{% else %}

Not sent: {{ i.send_error }}

{% endif %}
{{ i.body }}
{% if send_on %}
{% endif %} {% if i.state == 'queued' %}
{% else %}
{% endif %} {% call row_more() %} {% if i.state == 'queued' %}
{% endif %}
{% endcall %}
{{ skip_panel(i) }} {{ history(card) }} {% else %} {% if i.channel == 'email' and i.subject %}

{{ i.subject }}

{% endif %} {% if i.body %}
The message
{{ i.body }}
{% endif %}

{% if i.state == 'skipped' %}Skipped: {{ i.skip_reason }}{% if i.skip_note %} ({{ i.skip_note }}){% endif %}{% if i.skipped %}, {{ fmt_datetime(i.skipped) }}{% endif %} {% elif i.sent %}Sent {{ fmt_datetime(i.sent) }}{% if i.sent_with %}, logged{% else %}, marked by hand{% endif %}{% endif %} {% if i.edit_distance is not none %} · {{ i.edit_distance }} word{{ 's' if i.edit_distance != 1 }} edited{% endif %}

{% endif %} {% if i.state not in ('drafted', 'ready', 'queued', 'failed') %}{{ history(card) }}{% endif %}

{{ i.id }}{% if i.model %} · {{ i.model }}{% endif %}{% if i.seconds is not none %} · {{ i.seconds }}s{% endif %}{% if i.norms and not has_notes %} · norms: {{ i.norms | join('; ') }}{% endif %}

{% endfor %}
{% endblock %}